feat: AssetRepository SQLite + cycle need-review persistant (v0.10.0)
Table assets sur node:sqlite (Node 24, zéro dep native) : CRUD, listByStatus, incrementExec, setRiskTier anti-escalade, expireProvisional (cron PROVISOIRE→BLOQUÉ). 6 tests. Bump Node 24 (sqlite stable), Dockerfile 24.13 + copie tsconfig.build.json. 0 vuln. Palier de risque : reversible (persistance d'état, aucune mutation d'infra). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { AssetRepository } from "../src/gatekeeper/repository.js";
|
||||
import { createAsset, EscalationError, PROVISIONAL_TTL_MS } from "../src/gatekeeper/assets.js";
|
||||
|
||||
let repo: AssetRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new AssetRepository(":memory:");
|
||||
});
|
||||
afterEach(() => {
|
||||
repo.close();
|
||||
});
|
||||
|
||||
describe("AssetRepository", () => {
|
||||
it("crée et relit un asset", () => {
|
||||
const a = createAsset({ id: "a", type: "tool", version: "1.0.0", riskTier: "reversible", now: 1000 });
|
||||
repo.create(a);
|
||||
expect(repo.get("a")).toEqual(a);
|
||||
});
|
||||
|
||||
it("liste par statut", () => {
|
||||
repo.create(createAsset({ id: "r", type: "tool", version: "1.0.0", riskTier: "reversible" }));
|
||||
repo.create(createAsset({ id: "p", type: "stack-portainer", version: "1.0.0", riskTier: "privileged" }));
|
||||
expect(repo.listByStatus("provisoire").map((x) => x.id)).toEqual(["r"]);
|
||||
expect(repo.listByStatus("bloqué").map((x) => x.id)).toEqual(["p"]);
|
||||
});
|
||||
|
||||
it("incrémente le compteur d'exécution", () => {
|
||||
repo.create(createAsset({ id: "a", type: "tool", version: "1.0.0", riskTier: "reversible" }));
|
||||
expect(repo.incrementExec("a")).toBe(1);
|
||||
expect(repo.incrementExec("a")).toBe(2);
|
||||
});
|
||||
|
||||
it("setRiskTier refuse l'escalade privileged→reversible", () => {
|
||||
repo.create(createAsset({ id: "p", type: "stack-portainer", version: "1.0.0", riskTier: "privileged" }));
|
||||
expect(() => repo.setRiskTier("p", "reversible")).toThrow(EscalationError);
|
||||
// le palier n'a pas changé
|
||||
expect(repo.get("p")?.riskTier).toBe("privileged");
|
||||
});
|
||||
|
||||
it("expireProvisional bascule les provisoires échus en bloqué", () => {
|
||||
const t0 = 1_000_000;
|
||||
repo.create(createAsset({ id: "old", type: "tool", version: "1.0.0", riskTier: "reversible", now: t0 }));
|
||||
repo.create(createAsset({ id: "fresh", type: "tool", version: "1.0.0", riskTier: "reversible", now: t0 }));
|
||||
// bascule à un instant juste après l'expiration de "old" mais on garde "fresh" frais
|
||||
const switched = repo.expireProvisional(t0 + PROVISIONAL_TTL_MS + 1);
|
||||
expect(switched).toEqual(expect.arrayContaining(["old", "fresh"]));
|
||||
expect(repo.get("old")?.status).toBe("bloqué");
|
||||
});
|
||||
|
||||
it("n'expire rien avant l'échéance", () => {
|
||||
const t0 = 1_000_000;
|
||||
repo.create(createAsset({ id: "a", type: "tool", version: "1.0.0", riskTier: "reversible", now: t0 }));
|
||||
expect(repo.expireProvisional(t0 + 1)).toEqual([]);
|
||||
expect(repo.get("a")?.status).toBe("provisoire");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user