/* RFC-1213 AC tests: the verifier boundary — a valid+bound presentation yields a verified verdict on the result (AC-2); absent/invalid presentations reject with presentation-required/presentation-invalid (AC-3); the journaled record carries a presentation hash and no claim values (AC-5); a cryptographically valid VP bound to a different agentId is presentation-invalid (AC-8); an expired credential is presentation-invalid (AC-9). Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1213: initial test coverage. */ import { test, expect } from "vitest"; import { executeAction } from "./action-pipeline.ts"; import type { PresentationVerifier } from "./presentation.ts"; import { buildAgentSurfaceManifest } from "@warpgogol/werkstatt-shared/agent"; import type { PresentationJournalRecord } from "@warpgogol/werkstatt-shared/agent"; import type { CapabilityRecord } from "@warpgogol/werkstatt-shared/ontology"; import type { AgentGatePorts } from "./ports.ts"; const MANIFEST = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "test-site", baseUrl: "https://test.example", languages: { default: "en", supported: ["en"] }, }); const GATED: CapabilityRecord = { id: "contract.sign", version: 1, kind: "action", sideEffect: "write", title: { en: "Sign a contract" }, description: { en: "Requires age proof." }, input: { type: "object", additionalProperties: false, properties: { email: { type: "string" } }, }, output: { type: "object", additionalProperties: false, properties: { receiptId: { type: "string" } }, }, requires: { entitlements: ["agent.actions"], sections: [], verifiablePresentation: { accept: [ { type: "AgeOver18Credential", claims: ["credentialSubject.ageOver"], issuers: ["did:web:issuer.example"], }, ], purpose: "Prove the caller's principal is an adult", bindTo: "verified-agent", }, }, integration: { eventKind: "lead", source: "agent" }, humanEquivalent: { sectionType: "send-message" }, limits: { perMinutePerIp: 10, maxPayloadBytes: 10240 }, }; /** A structurally plausible VC DM 2.0 presentation for the fake verifier. */ const VP = { "@context": ["https://www.w3.org/ns/credentials/v2"], type: ["VerifiablePresentation"], holder: "agent-42", verifiableCredential: [ { type: ["VerifiableCredential", "AgeOver18Credential"], issuer: "did:web:issuer.example", credentialSubject: { ageOver: true, pii: "must-never-leak" }, validFrom: "2025-01-01T00:00:00.000Z", validUntil: "2027-01-01T00:00:00.000Z", }, ], }; /** Fake verifier port — extracts facts as a real VC library would. */ const goodVerifier: PresentationVerifier = async (vp) => { const pres = vp as typeof VP; return { holder: pres.holder, credentials: pres.verifiableCredential.map((c) => ({ types: c.type, issuer: c.issuer, subject: c.credentialSubject as Record, validFrom: c.validFrom, validUntil: c.validUntil, })), }; }; function ports(opts: { journal?: PresentationJournalRecord[]; verifier?: PresentationVerifier; }): AgentGatePorts { return { knowledge: { read: async () => null }, dispatch: { send: async (event) => ({ receiptId: event.eventId, status: "accepted" as const, duplicate: false, submittedAt: "2026-01-01T00:00:00.000Z", }), }, now: () => new Date("2026-01-01T00:00:00.000Z"), presentationVerifier: opts.verifier, presentationJournal: opts.journal ? (r) => opts.journal!.push(r) : undefined, }; } function run(opts: { presentation?: unknown; verified?: string; journal?: PresentationJournalRecord[]; verifier?: PresentationVerifier | null; capability?: CapabilityRecord; }) { return executeAction({ capability: opts.capability ?? GATED, manifest: MANIFEST, ports: ports({ journal: opts.journal, verifier: opts.verifier === null ? undefined : (opts.verifier ?? goodVerifier), }), validatedValue: { email: "agent@example.com" }, agentIdentity: opts.verified ? { verifiedAgentId: opts.verified } : {}, presentation: opts.presentation, }); } test("AC-2: a valid bound presentation dispatches and binds a verified verdict", async () => { const result = await run({ presentation: VP, verified: "agent-42" }); expect(result.kind).toBe("receipt"); expect(result.verdict).toMatchObject({ verified: true, matchedRequirement: "AgeOver18Credential", subjectBinding: "agent-42", issuer: "did:web:issuer.example", }); }); test("AC-3: a missing presentation rejects with presentation-required", async () => { const result = await run({ verified: "agent-42" }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-required"); }); test("AC-3: anonymous callers can never satisfy the requirement", async () => { const result = await run({ presentation: VP }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-required"); }); test("AC-3: cryptographic failure rejects with presentation-invalid", async () => { const result = await run({ presentation: VP, verified: "agent-42", verifier: async () => null, }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-invalid"); }); test("AC-5: the journaled record carries a presentation hash and no claim values", async () => { const journal: PresentationJournalRecord[] = []; await run({ presentation: VP, verified: "agent-42", journal }); expect(journal).toHaveLength(1); const record = journal[0]!; expect(record.schema).toBe("gogol.presentation-verdict/1"); expect(record.presentationHash).toMatch(/^sha256:/); expect(record.verified).toBe(true); // Schema-level guarantee: serialized record must not contain the PII payload. expect(JSON.stringify(record)).not.toContain("must-never-leak"); expect(JSON.stringify(record)).not.toContain("ageOver"); }); test("AC-8: a valid presentation bound to a different agentId is presentation-invalid", async () => { const result = await run({ presentation: VP, verified: "agent-99" }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-invalid"); expect(result.verdict?.failureReason).toContain("holder-binding"); }); test("AC-9: a credential outside its validity window is presentation-invalid", async () => { const expired = { ...VP, verifiableCredential: [ { ...VP.verifiableCredential[0]!, validUntil: "2025-06-01T00:00:00.000Z" }, ], }; const result = await run({ presentation: expired, verified: "agent-42" }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-invalid"); }); test("issuer outside the allowlist is rejected", async () => { const wrongIssuer = { ...VP, verifiableCredential: [{ ...VP.verifiableCredential[0]!, issuer: "did:web:evil.example" }], }; const result = await run({ presentation: wrongIssuer, verified: "agent-42" }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-invalid"); }); test("an unresolved @name issuer ref fails closed, never degrades to any-issuer", async () => { const unresolved: CapabilityRecord = { ...GATED, requires: { ...GATED.requires, verifiablePresentation: { ...GATED.requires.verifiablePresentation!, accept: [ { type: "AgeOver18Credential", claims: ["credentialSubject.ageOver"], issuers: ["@eudi"], // unresolved — never equals a did:/https: issuer id }, ], }, }, }; const result = await run({ capability: unresolved, presentation: VP, verified: "agent-42" }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-invalid"); }); test("verifier port absent → fail closed with presentation-invalid", async () => { const result = await run({ presentation: VP, verified: "agent-42", verifier: null }); expect(result.kind).toBe("problem"); if (result.kind === "problem") expect(result.problem.code).toBe("presentation-invalid"); expect(result.verdict?.failureReason).toContain("verifier-unavailable"); }); test("a capability without the requirement dispatches unmodified", async () => { const plain: CapabilityRecord = { ...GATED, id: "lead.submit", requires: { entitlements: ["agent.actions"], sections: [] }, }; const result = await run({ capability: plain }); expect(result.kind).toBe("receipt"); expect(result.verdict).toBeUndefined(); });