/* RFC-1172: telemetry emission coverage — the adapter-side exactly-once contract (AC-1), duplicate replay flag (AC-3), rate-limit status (AC-4), negotiated MCP version (AC-5), response invariance + silent-drop semantics (AC-6), and the PII-free payload boundary. Mirrors how `astro.ts`'s withTrafficEmit composes gate call + emitAgentTraffic. Do not import astro.ts — it imports astro:env/server and is test-adjacency exempt. RFC-1172: initial telemetry test suite. RFC-1172: closed agent.traffic telemetry — ports.telemetry sink, TrafficRecorder + boundary emit via withTrafficEmit */ import { beforeEach, describe, expect, test } from "vitest"; import { buildAgentSurfaceManifest } from "@warpgogol/werkstatt-shared/agent"; import type { AgentTrafficEvent } from "@warpgogol/werkstatt-shared/agent"; import type { CapabilityRecord } from "@warpgogol/werkstatt-shared/ontology"; import { createAgentGate, __resetLimiterCache } from "./index.ts"; import type { AgentGatePorts } from "./ports.ts"; import { createFixedWindowLimiter } from "./limits.ts"; import { httpBodyHash } from "./action-pipeline.ts"; import { deriveTrafficStatus, emitAgentTraffic, type TrafficRecorder } from "./telemetry.ts"; const LEAD_SUBMIT: CapabilityRecord = { id: "lead.submit", version: 2, kind: "action", sideEffect: "write", title: { de: "Anfrage senden", en: "Send an inquiry" }, description: { de: "Sendet eine Anfrage.", en: "Sends an inquiry." }, input: { type: "object", required: ["message"], additionalProperties: false, properties: { message: { type: "string", minLength: 10, maxLength: 4000 } }, }, output: { type: "object", required: ["receiptId"], additionalProperties: false, properties: { receiptId: { type: "string" } }, }, integration: { eventKind: "lead", source: "agent" }, requires: { entitlements: [], sections: ["send-message"] }, humanEquivalent: { sectionType: "send-message" }, limits: { perMinutePerIp: 10, maxPayloadBytes: 16384 }, }; function makeManifest() { return buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "test-bundle", baseUrl: "https://test.example", languages: { default: "de", supported: ["de"] }, knowledge: [ { domain: "offer", url: "/api/agent/v1/offer.json", schema: "gogol.agent.knowledge/offer@1" }, ], actions: [ { id: "lead.submit", url: "/api/agent/v1/actions/lead.submit", title: LEAD_SUBMIT.title, inputSchemaRef: "#/components/schemas/lead.submit-input", entitlement: "agent.actions", }, ], }); } const PROTOCOL_VERSION_META = { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }; beforeEach(() => { // The gate keeps a module-global limiter cache — reset so fake factories apply. __resetLimiterCache(); }); function rpc(method: string, params?: unknown, id: string | number = 1) { const p = (params ?? {}) as Record; const meta = { ...PROTOCOL_VERSION_META, ...((p._meta as Record) ?? {}) }; return new Request("https://test.example/api/agent/mcp", { method: "POST", headers: { "content-type": "application/json", "Signature-Agent": "test-agent/1.0" }, body: JSON.stringify({ jsonrpc: "2.0", id, method, params: { ...p, _meta: meta } }), }); } function actionRequest(body: unknown, headers: Record = {}) { return new Request("https://test.example/api/agent/v1/actions/lead.submit", { method: "POST", headers: { "content-type": "application/json", "Signature-Agent": "test-agent/1.0", ...headers, }, body: JSON.stringify(body), }); } interface Collect { events: AgentTrafficEvent[]; ports: AgentGatePorts; } /** Ports with the telemetry collector the adapter's boundary emit targets. */ function makeCollectPorts(overrides: Partial = {}): Collect { const events: AgentTrafficEvent[] = []; const ports: AgentGatePorts = { knowledge: { read: async (path: string) => (path === "/api/agent/v1/offer.json" ? '{"data":{}}' : 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), telemetry: (e) => events.push(e), ...overrides, }; return { events, ports }; } /** Mirror of astro.ts withTrafficEmit — boundary emit after the gate call. */ async function emitBoundary( collect: Collect, protocol: AgentTrafficEvent["protocol"], response: Response, traffic: TrafficRecorder, durationMs = 7, ) { await emitAgentTraffic(collect.ports, { systemId: "test-bundle", protocol, response, traffic, durationMs, }); } describe("boundary emit — exactly once per terminal path (AC-1)", () => { test("action success → one success event with capability + observed identity", async () => { const collect = makeCollectPorts(); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleAction( "lead.submit", actionRequest({ message: "Please send me a proposal." }), traffic, ); await emitBoundary(collect, "http", res, traffic); expect(collect.events).toHaveLength(1); const e = collect.events[0]!; expect(e).toMatchObject({ type: "werkstatt.agent.traffic", systemId: "test-bundle", protocol: "http", operation: "action.call", capability: "lead.submit", status: "success", durationMs: 7, }); expect(e.observedIdentity).toEqual({ "Signature-Agent": "test-agent/1.0" }); }); test("action problem (unknown capability) → one rejected event with problemKind", async () => { const collect = makeCollectPorts(); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleAction( "ghost.cap", actionRequest({ message: "Please send me a proposal." }), traffic, ); await emitBoundary(collect, "http", res, traffic); expect(collect.events).toHaveLength(1); expect(collect.events[0]).toMatchObject({ operation: "action.call", capability: "ghost.cap", status: "rejected", }); expect(collect.events[0]!.problemKind).toBeTruthy(); }); test("MCP tools/list → one success event", async () => { const collect = makeCollectPorts(); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleMcp(rpc("tools/list"), traffic); await emitBoundary(collect, "mcp", res, traffic); expect(collect.events).toHaveLength(1); expect(collect.events[0]).toMatchObject({ protocol: "mcp", operation: "tools/list", capability: "tools/list", status: "success", }); }); test("MCP method-not-found → rejected inside 200 envelope", async () => { const collect = makeCollectPorts(); const gate = createAgentGate(makeManifest(), [], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleMcp(rpc("unknown/method"), traffic); await emitBoundary(collect, "mcp", res, traffic); expect(collect.events).toHaveLength(1); expect(collect.events[0]).toMatchObject({ operation: "other", capability: "unknown/method", status: "rejected", problemKind: "json-rpc:-32601", }); }); test("malformed JSON → rejected", async () => { const collect = makeCollectPorts(); const gate = createAgentGate(makeManifest(), [], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleMcp( new Request("https://test.example/api/agent/mcp", { method: "POST", body: "not-json" }), traffic, ); await emitBoundary(collect, "mcp", res, traffic); expect(collect.events).toHaveLength(1); expect(collect.events[0]).toMatchObject({ operation: "other", status: "rejected" }); }); }); describe("duplicate replay (AC-3)", () => { test("stored-receipt replay emits duplicate: true", async () => { const body = { message: "Please send me a proposal." }; const raw = JSON.stringify(body); const receipt = { receiptId: "r-1", status: "accepted" as const, duplicate: false, submittedAt: "2026-07-05T00:00:00.000Z", }; const collect = makeCollectPorts({ idempotency: { get: async () => ({ receipt, bodyHash: httpBodyHash(raw) }), put: async () => {}, }, }); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleAction( "lead.submit", new Request("https://test.example/api/agent/v1/actions/lead.submit", { method: "POST", headers: { "content-type": "application/json", "Idempotency-Key": "key-1" }, body: raw, }), traffic, ); expect((await res.json()).status).toBe("duplicate"); await emitBoundary(collect, "http", res, traffic); expect(collect.events).toHaveLength(1); expect(collect.events[0]).toMatchObject({ operation: "action.call", duplicate: true }); }); }); describe("rate-limit status (AC-4)", () => { test("limiter deny → status rate-limited", async () => { const collect = makeCollectPorts({ createRateLimiter: () => ({ check: () => ({ allowed: false, retryAfterSeconds: 30 }), }), }); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleAction( "lead.submit", actionRequest({ message: "Please send me a proposal." }), traffic, ); expect(res.status).toBe(429); await emitBoundary(collect, "http", res, traffic); expect(collect.events).toHaveLength(1); expect(collect.events[0]).toMatchObject({ status: "rate-limited" }); }); }); describe("negotiated MCP version (AC-5)", () => { test("tools/call carries mcpProtocolVersion from _meta", async () => { const collect = makeCollectPorts(); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleMcp(rpc("tools/call", { name: "knowledge.offer.get" }), traffic); await emitBoundary(collect, "mcp", res, traffic); expect(collect.events[0]).toMatchObject({ operation: "tools/call", capability: "knowledge.offer.get", mcpProtocolVersion: "2026-07-28", }); }); }); describe("emission never alters the response (AC-6)", () => { test("response identical with telemetry on vs off", async () => { const on = makeCollectPorts(); const off = makeCollectPorts({ telemetry: undefined }); const mk = (ports: AgentGatePorts) => createAgentGate(makeManifest(), [LEAD_SUBMIT], ports); const req = () => actionRequest({ message: "Please send me a proposal." }); const resOn = await mk(on.ports).handleAction("lead.submit", req(), {}); const resOff = await mk(off.ports).handleAction("lead.submit", req(), {}); expect(resOn.status).toBe(resOff.status); // receiptId is random per request — compare the deterministic fields. const { receiptId: _on, ...bodyOn } = (await resOn.json()) as Record; const { receiptId: _off, ...bodyOff } = (await resOff.json()) as Record; expect(bodyOn).toEqual(bodyOff); }); test("throwing sink is swallowed", async () => { const collect = makeCollectPorts({ telemetry: () => { throw new Error("sink exploded"); }, }); const traffic: TrafficRecorder = { operation: "health" }; await emitBoundary(collect, "http", new Response("{}", { status: 200 }), traffic); expect(collect.events).toHaveLength(0); // drop, not throw }); test("no telemetry port → silent no-op", async () => { const collect = makeCollectPorts({ telemetry: undefined }); await emitBoundary(collect, "http", new Response("{}", { status: 200 }), { operation: "health", }); expect(collect.events).toHaveLength(0); }); test("unset operation → no event (never emit a fake value)", async () => { const collect = makeCollectPorts(); await emitBoundary(collect, "mcp", new Response("{}", { status: 200 }), {}); expect(collect.events).toHaveLength(0); }); }); describe("PII boundary", () => { test("event JSON carries no request payload", async () => { const collect = makeCollectPorts(); const gate = createAgentGate(makeManifest(), [LEAD_SUBMIT], collect.ports); const traffic: TrafficRecorder = {}; const res = await gate.handleAction( "lead.submit", actionRequest({ message: "Please send me a proposal — secret details" }), traffic, ); await emitBoundary(collect, "http", res, traffic); const line = JSON.stringify(collect.events[0]); expect(line).not.toContain("secret details"); expect(line).not.toContain("message"); }); }); describe("deriveTrafficStatus", () => { test("2xx → success; 429 → rate-limited; 4xx → rejected; 5xx → failure", () => { expect(deriveTrafficStatus(new Response(null, { status: 200 }))).toBe("success"); expect(deriveTrafficStatus(new Response(null, { status: 202 }))).toBe("success"); expect(deriveTrafficStatus(new Response(null, { status: 429 }))).toBe("rate-limited"); expect(deriveTrafficStatus(new Response(null, { status: 400 }))).toBe("rejected"); expect(deriveTrafficStatus(new Response(null, { status: 404 }))).toBe("rejected"); expect(deriveTrafficStatus(new Response(null, { status: 502 }))).toBe("failure"); }); });