import { describe, expect, it } from "vitest"; import { asDecisionSource, type DecisionSource, MAX_DECISION_SOURCE_DEPTH, } from "#src/authority/decision-source"; /** Wrap `inner` in `depth` nested `forwarded` frames. */ function nest(depth: number, inner: DecisionSource): DecisionSource { let source = inner; for (let i = 0; i < depth; i++) { source = { kind: "forwarded", responderSessionId: `session-${i}`, decision: source, }; } return source; } describe("asDecisionSource", () => { describe("round-trips every variant", () => { const variants: readonly DecisionSource[] = [ { kind: "user", via: "dialog" }, { kind: "user", via: "select" }, { kind: "authorizer", name: "model-judge", verdict: "deny", reason: "reads outside the project", }, { kind: "authorizer", name: "model-judge", verdict: "allow", reason: null, }, { kind: "rule", surface: "external_directory", pattern: "/tmp/*", origin: "global", }, { kind: "rule", surface: "bash", pattern: null, origin: null }, { kind: "session_approval", surface: "external_directory", pattern: "/tmp/*", }, { kind: "session_approval", surface: "bash", pattern: null }, { kind: "yolo", pattern: "" }, { kind: "yolo", pattern: null }, { kind: "infrastructure_read" }, { kind: "unavailable", reason: "Session 'abc' did not answer within 600s", }, { kind: "gate_error", reason: "boom" }, { kind: "forwarded", responderSessionId: "019ff969-c34c-70be-9034-fae19c852932", decision: { kind: "user", via: "dialog" }, }, { kind: "forwarded", responderSessionId: null, decision: null }, ]; for (const variant of variants) { it(`admits ${variant.kind} (${JSON.stringify(variant)})`, () => { expect(asDecisionSource(JSON.parse(JSON.stringify(variant)))).toEqual( variant, ); }); } }); describe("rejects malformed input", () => { it.each([ ["null", null], ["a string", "user"], ["an array", [{ kind: "user", via: "dialog" }]], ["an unknown kind", { kind: "telepathy" }], ["a missing kind", { via: "dialog" }], ["an unknown user surface", { kind: "user", via: "smoke-signal" }], ["a missing user surface", { kind: "user" }], [ "an unknown authorizer verdict", { kind: "authorizer", name: "j", verdict: "defer", reason: null }, ], [ "a missing authorizer name", { kind: "authorizer", verdict: "allow", reason: null }, ], [ "a non-string authorizer name", { kind: "authorizer", name: 7, verdict: "allow", reason: null }, ], ["a missing rule surface", { kind: "rule", pattern: null, origin: null }], [ "a non-nullable-string rule pattern", { kind: "rule", surface: "bash", pattern: 7, origin: null }, ], [ "a missing session_approval surface", { kind: "session_approval", pattern: null }, ], ["a missing yolo pattern", { kind: "yolo" }], ["a missing unavailable reason", { kind: "unavailable" }], ["a non-string gate_error reason", { kind: "gate_error", reason: null }], [ "a missing forwarded decision", { kind: "forwarded", responderSessionId: "s" }, ], [ "a non-nullable-string responderSessionId", { kind: "forwarded", responderSessionId: 7, decision: null }, ], ])("rejects %s", (_label, value) => { expect(asDecisionSource(value)).toBeUndefined(); }); it("rejects the whole value when a nested decision is malformed", () => { // All-or-nothing, like `asPromptPayload`: a half-parsed provenance record // would assert a decider that never decided. expect( asDecisionSource({ kind: "forwarded", responderSessionId: "session-1", decision: { kind: "user", via: "smoke-signal" }, }), ).toBeUndefined(); }); it("drops unknown properties rather than rejecting", () => { expect( asDecisionSource({ kind: "user", via: "dialog", clicks: 2 }), ).toEqual({ kind: "user", via: "dialog" }); }); }); describe("bounds nesting depth", () => { it("admits nesting up to the bound", () => { const source = nest(MAX_DECISION_SOURCE_DEPTH, { kind: "user", via: "dialog", }); expect(asDecisionSource(JSON.parse(JSON.stringify(source)))).toEqual( source, ); }); it("rejects nesting past the bound", () => { const source = nest(MAX_DECISION_SOURCE_DEPTH + 1, { kind: "user", via: "dialog", }); expect( asDecisionSource(JSON.parse(JSON.stringify(source))), ).toBeUndefined(); }); }); });