Files
chlova/orchestrator/test/review.test.ts
T
Kantin-Petit a193b4e912 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>
2026-06-23 01:32:18 +02:00

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/);
});
});