import { describe, it, expect } from "vitest"; import { ReadOnlyGuard } from "../src/gatekeeper/guard.js"; import { createAsset, assertNoEscalation, canExecute, EscalationError, PROVISIONAL_TTL_MS, } from "../src/gatekeeper/assets.js"; import type { ToolSpec } from "../src/agent/types.js"; const spec = (over: Partial): ToolSpec => ({ name: "n8n.list_workflows", description: "", parameters: {}, server: "n8n", readOnly: true, riskTier: "reversible", ...over, }); describe("ReadOnlyGuard (Phase 1)", () => { const guard = new ReadOnlyGuard(); it("autorise un outil reversible + readOnly", () => { expect(guard.authorize(spec({})).allowed).toBe(true); }); it("refuse un outil privilégié", () => { const v = guard.authorize(spec({ riskTier: "privileged", readOnly: false })); expect(v.allowed).toBe(false); expect(v.reason).toMatch(/privilégié/); }); it("refuse un outil reversible mais non read-only (défense en profondeur)", () => { const v = guard.authorize(spec({ riskTier: "reversible", readOnly: false })); expect(v.allowed).toBe(false); }); }); describe("paliers de risque & sursis", () => { it("réversible → PROVISOIRE avec sursis de 7 jours", () => { const t0 = 1_000_000; const a = createAsset({ id: "a", type: "workflow-n8n", version: "1.0.0", riskTier: "reversible", now: t0 }); expect(a.status).toBe("provisoire"); expect(a.expiresAt).toBe(t0 + PROVISIONAL_TTL_MS); }); it("privilégié → BLOQUÉ, AUCUN sursis", () => { const a = createAsset({ id: "b", type: "stack-portainer", version: "1.0.0", riskTier: "privileged" }); expect(a.status).toBe("bloqué"); expect(a.expiresAt).toBeNull(); }); }); describe("invariant anti-escalade", () => { it("interdit privileged → reversible", () => { expect(() => assertNoEscalation("privileged", "reversible")).toThrow(EscalationError); }); it("autorise reversible → privileged (durcissement)", () => { expect(() => assertNoEscalation("reversible", "privileged")).not.toThrow(); }); it("autorise les reclassements identiques", () => { expect(() => assertNoEscalation("privileged", "privileged")).not.toThrow(); expect(() => assertNoEscalation("reversible", "reversible")).not.toThrow(); }); }); describe("canExecute (gatekeeper Phase 2, posé)", () => { it("bloque un provisoire expiré", () => { const t0 = 1_000_000; const a = createAsset({ id: "c", type: "tool", version: "1.0.0", riskTier: "reversible", now: t0 }); expect(canExecute(a, t0 + 1).ok).toBe(true); expect(canExecute(a, t0 + PROVISIONAL_TTL_MS + 1).ok).toBe(false); }); it("bloque un asset bloqué et un refusé", () => { const a = createAsset({ id: "d", type: "tool", version: "1.0.0", riskTier: "privileged" }); expect(canExecute(a).ok).toBe(false); }); });