a193b4e912
ReviewService (approuver/refuser/lister) + cron horaire PROVISOIRE→BLOQUÉ. Commandes Telegram owner /pending /approve /refuse hors boucle agent (le LLM ne peut pas décider de la review). Câblage Phase 2 : routage commande/agent, cron démarré + arrêt propre. 45 tests verts. Palier de risque : reversible (contrôle humain ; n'exécute aucune mutation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { AssetRepository } from "../src/gatekeeper/repository.js";
|
|
import { ReviewService, runExpiryOnce } from "../src/gatekeeper/review.js";
|
|
import { handleReviewCommand, isCommand } from "../src/surfaces/commands.js";
|
|
import { createAsset, PROVISIONAL_TTL_MS } from "../src/gatekeeper/assets.js";
|
|
import { createLogger } from "../src/audit/log.js";
|
|
|
|
const log = createLogger("silent");
|
|
let repo: AssetRepository;
|
|
let review: ReviewService;
|
|
|
|
beforeEach(() => {
|
|
repo = new AssetRepository(":memory:");
|
|
review = new ReviewService(repo, log);
|
|
});
|
|
afterEach(() => repo.close());
|
|
|
|
describe("ReviewService", () => {
|
|
it("approuve un asset bloqué", () => {
|
|
repo.create(createAsset({ id: "p", type: "stack-portainer", version: "1.0.0", riskTier: "privileged" }));
|
|
expect(review.approve("p").status).toBe("approuvé");
|
|
expect(repo.get("p")?.status).toBe("approuvé");
|
|
});
|
|
|
|
it("refuse un asset", () => {
|
|
repo.create(createAsset({ id: "p", type: "tool", version: "1.0.0", riskTier: "privileged" }));
|
|
expect(review.refuse("p").status).toBe("refusé");
|
|
});
|
|
|
|
it("lève sur asset inconnu", () => {
|
|
expect(() => review.approve("nope")).toThrow(/inconnu/);
|
|
});
|
|
|
|
it("listPending renvoie bloqués + provisoires", () => {
|
|
repo.create(createAsset({ id: "b", type: "tool", version: "1.0.0", riskTier: "privileged" }));
|
|
repo.create(createAsset({ id: "r", type: "tool", version: "1.0.0", riskTier: "reversible" }));
|
|
expect(review.listPending().map((a) => a.id).sort()).toEqual(["b", "r"]);
|
|
});
|
|
});
|
|
|
|
describe("runExpiryOnce", () => {
|
|
it("bascule les provisoires échus", () => {
|
|
const t0 = Date.now() - PROVISIONAL_TTL_MS - 1000;
|
|
repo.create(createAsset({ id: "old", type: "tool", version: "1.0.0", riskTier: "reversible", now: t0 }));
|
|
expect(runExpiryOnce(repo, log)).toEqual(["old"]);
|
|
expect(repo.get("old")?.status).toBe("bloqué");
|
|
});
|
|
});
|
|
|
|
describe("commandes de review", () => {
|
|
it("isCommand détecte le préfixe /", () => {
|
|
expect(isCommand("/pending")).toBe(true);
|
|
expect(isCommand("bonjour")).toBe(false);
|
|
});
|
|
|
|
it("/pending liste ou indique vide", () => {
|
|
expect(handleReviewCommand(review, "/pending")).toMatch(/Aucun/);
|
|
repo.create(createAsset({ id: "p", type: "tool", version: "1.0.0", riskTier: "privileged" }));
|
|
expect(handleReviewCommand(review, "/pending")).toContain("p");
|
|
});
|
|
|
|
it("/approve <id> approuve", () => {
|
|
repo.create(createAsset({ id: "p", type: "tool", version: "1.0.0", riskTier: "privileged" }));
|
|
expect(handleReviewCommand(review, "/approve p")).toMatch(/approuvé/);
|
|
expect(repo.get("p")?.status).toBe("approuvé");
|
|
});
|
|
|
|
it("/approve sans id renvoie l'usage", () => {
|
|
expect(handleReviewCommand(review, "/approve")).toMatch(/Usage/);
|
|
});
|
|
|
|
it("commande inconnue renvoie l'aide", () => {
|
|
expect(handleReviewCommand(review, "/wat")).toMatch(/inconnue/);
|
|
});
|
|
});
|