Files
chlova/orchestrator/test/gatekeeper.test.ts
T
Kantin-Petit 1cce8c9db6 test: gatekeeper + readonly-filter + config, interfaces need-review (v0.9.0)
Fin Phase 1. 22 tests verts : barrière readonly-filter (fail-safe),
ReadOnlyGuard, paliers de risque + sursis, invariant anti-escalade,
config fail-closed + masquage secrets. Interfaces du cycle need-review
posées pour la Phase 2 (Asset, canExecute) sans câblage runtime. Split
tsconfig typecheck/build.

Palier de risque : reversible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:18:02 +02:00

84 lines
2.8 KiB
TypeScript

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