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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user