import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { AssetRepository } from "../src/gatekeeper/repository.js"; import { Gatekeeper } from "../src/gatekeeper/gatekeeper.js"; import { runAlertCycleOnce } from "../src/alerts/scheduler.js"; import { createAsset, PROVISIONAL_TTL_MS } from "../src/gatekeeper/assets.js"; import { createLogger } from "../src/audit/log.js"; import type { AlertSender, AlertEvent } from "../src/alerts/types.js"; import type { ToolSpec } from "../src/agent/types.js"; const log = createLogger("silent"); class CapturingSender implements AlertSender { events: AlertEvent[] = []; async send(e: AlertEvent): Promise { this.events.push(e); } } let repo: AssetRepository; beforeEach(() => (repo = new AssetRepository(":memory:"))); afterEach(() => repo.close()); describe("Gatekeeper first_provisional_exec", () => { it("alerte à la 1ʳᵉ exécution d'un asset PROVISOIRE puis plus", () => { const onFirstProvisionalExec = vi.fn(); const gk = new Gatekeeper(repo, log, { onFirstProvisionalExec }); // asset provisoire pré-existant, exécutable via la capacité privilégiée const id = "tool:portainer.redeploy"; repo.create(createAsset({ id, type: "tool", version: "1.0.0", riskTier: "reversible" })); repo.updateStatus(id, "provisoire"); const spec: ToolSpec = { name: "portainer.redeploy", description: "", parameters: {}, server: "portainer", readOnly: false, riskTier: "privileged", }; expect(gk.authorizeTool(spec).allowed).toBe(true); expect(gk.authorizeTool(spec).allowed).toBe(true); expect(onFirstProvisionalExec).toHaveBeenCalledOnce(); // une seule fois expect(repo.get(id)?.execCount).toBe(2); }); }); describe("runAlertCycleOnce", () => { it("émet J-1 + digest quand des assets sont en attente", async () => { const now = 2_000_000_000; // provisoire qui expire dans ~1h → J-1 repo.create(createAsset({ id: "soon", type: "tool", version: "1.0.0", riskTier: "reversible", now: now - PROVISIONAL_TTL_MS + 3_600_000 })); // privilégié bloqué → compte dans le digest repo.create(createAsset({ id: "blk", type: "stack-portainer", version: "1.0.0", riskTier: "privileged", now })); const sender = new CapturingSender(); await runAlertCycleOnce(repo, sender, now); const kinds = sender.events.map((e) => e.kind); expect(kinds).toContain("countdown_j1"); expect(kinds).toContain("daily_digest"); }); it("n'émet rien sans asset en attente", async () => { const sender = new CapturingSender(); await runAlertCycleOnce(repo, sender, Date.now()); expect(sender.events).toHaveLength(0); }); });