feat: cron expiration + commandes de review owner (v0.13.0)
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>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user