/* RFC-1114: the A2A conformance corpus — golden request/response pairs replayed through createAgentGate.handleA2a with fake ports. The pinned subset is SendMessage over JSON-RPC 2.0 with the A2A-Version header gate; the handler must satisfy these fixtures. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1114: initial A2A conformance corpus. RFC-1114: minimal real A2A endpoint + honest agent card */ import { test, expect } from "vitest"; import { createAgentGate, type AgentGatePorts, __resetLimiterCache } from "../../index.ts"; import { __resetA2aLimiterCache } from "../handler.ts"; import { createFixedWindowLimiter } from "../../limits.ts"; import { buildAgentSurfaceManifest, type ActionReceipt } from "@warpgogol/werkstatt-shared/agent"; import type { CapabilityRecord } from "@warpgogol/werkstatt-shared/ontology"; const LEAD_SUBMIT: CapabilityRecord = { id: "lead.submit", version: 2, kind: "action", sideEffect: "write", title: { de: "Anfrage senden", en: "Submit an inquiry" }, description: { de: "Übermittelt eine Anfrage.", en: "Delivers an inquiry." }, input: { type: "object", required: ["message"], additionalProperties: false, properties: { message: { type: "string", minLength: 10, maxLength: 4000 } }, }, output: { type: "object", required: ["receiptId", "status", "duplicate", "submittedAt"], additionalProperties: false, properties: { receiptId: { type: "string", format: "uuid" }, status: { type: "string" }, duplicate: { type: "boolean" }, submittedAt: { type: "string" }, }, }, integration: { eventKind: "lead", source: "agent" }, requires: { entitlements: [], sections: ["send-message"] }, humanEquivalent: { sectionType: "send-message" }, limits: { perMinutePerIp: 10, maxPayloadBytes: 16384 }, }; /** RFC-1169: v2 readiness envelope + nextAction linkage. */ const LEAD_PREPARE: CapabilityRecord = { id: "lead.prepare", version: 2, kind: "action", sideEffect: "none", title: { de: "Anfrage prüfen", en: "Preview an inquiry" }, description: { de: "Prüft eine Anfrage.", en: "Validates an inquiry." }, input: { type: "object", required: ["message"], additionalProperties: false, properties: { message: { type: "string", minLength: 10, maxLength: 4000 } }, }, output: { type: "object", required: ["ready"], additionalProperties: false, properties: { ready: { type: "boolean" }, draftId: { type: "string" }, }, }, nextAction: { capability: "lead.submit", sideEffect: "Creates a project inquiry and dispatches it to the site owner.", }, requires: { entitlements: [], sections: [] }, limits: { perMinutePerIp: 10, maxPayloadBytes: 16384 }, }; function makeManifest(actionIds: string[] = ["lead.submit"]) { return buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "test-bundle", baseUrl: "https://test.example", languages: { default: "de", supported: ["de", "en"] }, knowledge: [], actions: actionIds.map((id) => ({ id, url: `/api/agent/v1/actions/${id}`, title: { de: id }, inputSchemaRef: `#/components/schemas/${id}-input`, entitlement: "agent.actions", })), a2a: { url: "/api/agent/a2a", protocolVersion: "1.0" }, }); } function makeFakePorts(overrides: Partial = {}): AgentGatePorts { return { knowledge: { read: async () => null }, dispatch: { send: async (event) => ({ receiptId: event.eventId, status: "accepted" as const, duplicate: false, submittedAt: "2026-07-05T00:00:00.000Z", }), }, now: () => new Date("2026-07-05T00:00:00.000Z"), createRateLimiter: (maxPerWindow) => createFixedWindowLimiter(60, maxPerWindow), ...overrides, }; } function sendMessage( parts: unknown[], opts: { messageId?: string; contextId?: string; headers?: Record } = {}, ) { return new Request("https://test.example/api/agent/a2a", { method: "POST", headers: { "content-type": "application/json", ...opts.headers }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "SendMessage", params: { message: { messageId: opts.messageId ?? "msg-1", role: "user", ...(opts.contextId ? { contextId: opts.contextId } : {}), parts, }, }, }), }); } function capabilityPart(capability: string, input: Record) { return [{ kind: "data", data: { capability, input } }]; } function rawRpc(body: unknown, headers: Record = {}) { return new Request("https://test.example/api/agent/a2a", { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body), }); } interface A2aTaskBody { result?: { id: string; contextId?: string; status: { state: string; timestamp: string }; artifacts?: { name: string; parts: { kind: string; data: unknown }[] }[]; }; error?: { code: number; message: string; data?: { code?: string; retryAfterSeconds?: number } }; } // --------------------------------------------------------------------------- // AC-1: happy path — completed task whose artifact carries the ActionReceipt // --------------------------------------------------------------------------- test("SendMessage: valid capability DataPart returns a completed task with the receipt artifact", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" })), ); expect(res.status).toBe(200); const body = (await res.json()) as A2aTaskBody; const task = body.result!; expect(task.status.state).toBe("completed"); expect(task.status.timestamp).toBe("2026-07-05T00:00:00.000Z"); const receipt = task.artifacts![0]!.parts[0]!.data as { receiptId: string; status: string; duplicate: boolean; submittedAt: string; }; expect(task.artifacts![0]!.name).toBe("receipt"); expect(task.id).toBe(receipt.receiptId); expect(receipt.status).toBe("accepted"); expect(receipt.duplicate).toBe(false); expect(receipt.receiptId).toMatch(/^[0-9a-f-]{36}$/); }); test("SendMessage: contextId is echoed back on the task", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" }), { contextId: "ctx-42", }), ); const body = (await res.json()) as A2aTaskBody; expect(body.result!.contextId).toBe("ctx-42"); }); // --------------------------------------------------------------------------- // AC-2: unknown capability / missing capability DataPart → -32602 // --------------------------------------------------------------------------- test("SendMessage: unknown capability returns -32602 with problem details in error.data", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("does.not.exist", { message: "hello world!" })), ); const body = (await res.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32602); expect(body.error!.data!.code).toBe("unknown-capability"); }); test("SendMessage: no capability DataPart returns -32602", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a(sendMessage([{ kind: "text", text: "hello there" }])); const body = (await res.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32602); expect(body.error!.data!.code).toBe("schema-violation"); }); test("SendMessage: malformed message (missing messageId) returns -32602", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( rawRpc({ jsonrpc: "2.0", id: 1, method: "SendMessage", params: { message: { role: "user", parts: [] } }, }), ); const body = (await res.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32602); }); // --------------------------------------------------------------------------- // Terminal-state tasks: rejected (schema) / failed (dispatch) / preview // --------------------------------------------------------------------------- test("SendMessage: schema violation returns a rejected task with the problem artifact", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "short" })), ); expect(res.status).toBe(200); const body = (await res.json()) as A2aTaskBody; const task = body.result!; expect(task.status.state).toBe("rejected"); const problem = task.artifacts![0]!.parts[0]!.data as { code: string; errors: unknown[] }; expect(task.artifacts![0]!.name).toBe("problem"); expect(problem.code).toBe("schema-violation"); expect(problem.errors.length > 0).toBeTruthy(); }); test("SendMessage: dispatch failure returns a failed task with the problem artifact", async () => { const gate = createAgentGate( makeManifest(), [LEAD_SUBMIT], makeFakePorts({ dispatch: { send: async () => { throw new Error("boom"); }, }, }), ); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" })), ); const body = (await res.json()) as A2aTaskBody; const task = body.result!; expect(task.status.state).toBe("failed"); const problem = task.artifacts![0]!.parts[0]!.data as { code: string; retryable: boolean }; expect(problem.code).toBe("dispatch-failed"); expect(problem.retryable).toBe(true); }); test("SendMessage: sideEffect none capability returns a completed task with the preview envelope", async () => { const gate = createAgentGate(makeManifest(["lead.prepare"]), [LEAD_PREPARE], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.prepare", { message: "hello world!" })), ); const body = (await res.json()) as A2aTaskBody; const task = body.result!; expect(task.status.state).toBe("completed"); expect(task.artifacts![0]!.name).toBe("preview"); const preview = task.artifacts![0]!.parts[0]!.data as { ready: boolean; draftId: string; summary: Record; nextAction: string; sideEffect: string; }; // RFC-1169 AC-9: identical envelope fields on A2A — parity test pins the shape. expect(preview.ready).toBe(true); expect(preview.draftId).toMatch(/^sha256:[0-9a-f]{64}$/); expect(preview.summary).toEqual({ message: "hello world!" }); expect(preview.nextAction).toBe("lead.submit"); expect(preview.sideEffect).toBe("Creates a project inquiry and dispatches it to the site owner."); }); test("SendMessage: preview with missing fields completes with a ready:false artifact (not rejected)", async () => { const gate = createAgentGate(makeManifest(["lead.prepare"]), [LEAD_PREPARE], makeFakePorts()); const res = await gate.handleA2a(sendMessage(capabilityPart("lead.prepare", {}))); const body = (await res.json()) as A2aTaskBody; const task = body.result!; // RFC-1169: incomplete input is a normal preview answer, not a refusal — // "completed" task carrying the readiness envelope. expect(task.status.state).toBe("completed"); expect(task.artifacts![0]!.name).toBe("preview"); const preview = task.artifacts![0]!.parts[0]!.data as { ready: boolean; missing: string[]; problems: Array<{ field: string; code: string; detail: string }>; }; expect(preview.ready).toBe(false); expect(preview.missing).toEqual(["message"]); expect(preview.problems).toEqual([]); }); // --------------------------------------------------------------------------- // AC-6: payload cap + rate limit parity with the MCP path // --------------------------------------------------------------------------- test("SendMessage: oversized input returns -32000 with payload-too-large", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "x".repeat(20000) })), ); const body = (await res.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32000); expect(body.error!.data!.code).toBe("payload-too-large"); }); test("SendMessage: per-IP rate limit returns -32000 with rate-limited + retryAfterSeconds", async () => { __resetA2aLimiterCache(); __resetLimiterCache(); const capped: CapabilityRecord = { ...LEAD_SUBMIT, limits: { perMinutePerIp: 2, maxPayloadBytes: 16384 }, }; const gate = createAgentGate(makeManifest(), [capped], makeFakePorts()); const req = () => new Request("https://test.example/api/agent/a2a", { method: "POST", headers: { "content-type": "application/json", "CF-Connecting-IP": "1.2.3.4" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "SendMessage", params: { message: { messageId: crypto.randomUUID(), role: "user", parts: capabilityPart("lead.submit", { message: "hello world!" }), }, }, }), }); expect((await gate.handleA2a(req())).status).toBe(200); expect((await gate.handleA2a(req())).status).toBe(200); const r3 = await gate.handleA2a(req()); const body = (await r3.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32000); expect(body.error!.data!.code).toBe("rate-limited"); expect(body.error!.data!.retryAfterSeconds).toBeGreaterThan(0); }); // --------------------------------------------------------------------------- // Version gate + transport boundary // --------------------------------------------------------------------------- test("A2A-Version: header 0.3 is rejected with -32008 + HTTP 400", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" }), { headers: { "A2A-Version": "0.3" }, }), ); expect(res.status).toBe(400); const body = (await res.json()) as { error: { code: number; data: { supported: string[]; requested: string } }; }; expect(body.error.code).toBe(-32008); expect(body.error.data.supported).toEqual(["1.0"]); expect(body.error.data.requested).toBe("0.3"); }); test("A2A-Version: header 1.0 proceeds; absent header is served as 1.0", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const withHeader = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" }), { headers: { "A2A-Version": "1.0" }, }), ); expect(withHeader.status).toBe(200); const without = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" })), ); expect(without.status).toBe(200); }); test("unknown method returns -32601 method-not-found", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( rawRpc({ jsonrpc: "2.0", id: 1, method: "GetTask", params: { id: "t1" } }), ); const body = (await res.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32601); }); test("batch requests are rejected with -32600 invalid-request", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a(rawRpc([{ jsonrpc: "2.0", id: 1, method: "SendMessage" }])); const body = (await res.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32600); }); test("GET is rejected with 405", async () => { const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( new Request("https://test.example/api/agent/a2a", { method: "GET" }), ); expect(res.status).toBe(405); }); // --------------------------------------------------------------------------- // RFC-1112 parity: idempotency replay + disabled marker // --------------------------------------------------------------------------- function makeMemoryStore() { const map = new Map(); return { get: async (key: string) => map.get(key) ?? null, put: async (key: string, bodyHash: string, receipt: ActionReceipt) => { map.set(key, { receipt, bodyHash }); }, }; } test("SendMessage: Idempotency-Key replay completes the task with the stored receipt as duplicate", async () => { __resetA2aLimiterCache(); const store = makeMemoryStore(); let dispatchCount = 0; const gate = createAgentGate( makeManifest(), [LEAD_SUBMIT], makeFakePorts({ idempotency: store, dispatch: { send: async (event) => { dispatchCount += 1; return { receiptId: event.eventId, status: "accepted" as const, duplicate: false, submittedAt: "2026-07-05T00:00:00.000Z", }; }, }, }), ); const req = () => sendMessage(capabilityPart("lead.submit", { message: "hello world!" }), { headers: { "Idempotency-Key": "key-1" }, }); const r1 = await gate.handleA2a(req()); const first = ((await r1.json()) as A2aTaskBody).result!; const r2 = await gate.handleA2a(req()); const second = ((await r2.json()) as A2aTaskBody).result!; expect(second.id).toBe(first.id); const receipt = second.artifacts![0]!.parts[0]!.data as { duplicate: boolean; status: string }; expect(receipt.duplicate).toBe(true); expect(receipt.status).toBe("duplicate"); expect(dispatchCount).toBe(1); }); test("SendMessage: no idempotency port → X-Agent-Idempotency: disabled", async () => { __resetA2aLimiterCache(); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" }), { headers: { "Idempotency-Key": "key-2" }, }), ); expect(res.headers.get("X-Agent-Idempotency")).toBe("disabled"); }); test("SendMessage: zero active capabilities still answers — every call resolves to -32602", async () => { const gate = createAgentGate(makeManifest([]), [], makeFakePorts()); const res = await gate.handleA2a( sendMessage(capabilityPart("lead.submit", { message: "hello world!" })), ); const body = (await res.json()) as A2aTaskBody; expect(body.error!.code).toBe(-32602); expect(body.error!.data!.code).toBe("unknown-capability"); });