/* RFC-0289: bijection + shape tests for the OpenAPI projection formatter. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-0289: initial OpenAPI formatter tests. RFC-0954: add search path projection tests. RFC-1113: align agent discovery surface with RFC 9727 and MCP 2026-07-28 stateless era */ import { test, expect } from "vitest"; import { formatAgentOpenApi } from "../openapi.ts"; import { buildAgentSurfaceManifest } from "../manifest.ts"; import type { CapabilityOutputSchema } from "@warpgogol/werkstatt-shared/ontology"; test("formatAgentOpenApi: empty manifest yields a valid document with no paths", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, }); const doc = formatAgentOpenApi(manifest, []); expect(doc.openapi).toBe("3.1.0"); expect(doc.info.title).toBe("s agent surface"); expect(doc.info["x-gogol-content-hash"]).toBe(manifest.contentHash); expect(doc.paths).toEqual({}); expect("x-gogol-mcp" in doc).toBe(false); expect("x-gogol-proof" in doc).toBe(false); }); test("formatAgentOpenApi: one knowledge ref → one GET path + one component schema", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, knowledge: [ { domain: "offer", url: "/api/agent/v1/offer.json", schema: "gogol.agent.knowledge/offer@1" }, ], }); const doc = formatAgentOpenApi(manifest, []); const op = doc.paths["/api/agent/v1/offer.json"]?.get; expect(op).toBeTruthy(); expect(op!.operationId).toBe("knowledge.offer.get"); expect(op!.tags).toEqual(["knowledge"]); expect(doc.components.schemas["knowledge-offer"]).toBeTruthy(); }); test("formatAgentOpenApi: one action ref → one POST path with verbatim input/output schemas", () => { const inputSchema = { type: "object" as const, additionalProperties: false as const, required: ["message"], properties: { message: { type: "string" as const } }, }; const outputSchema = { type: "object" as const, additionalProperties: false as const, properties: { accepted: { type: "boolean" as const } }, }; const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, actions: [ { id: "lead.submit", url: "/api/agent/v1/actions/lead.submit", title: { de: "Anfrage senden" }, inputSchemaRef: "#/components/schemas/lead.submit-input", entitlement: "agent.actions", }, ], }); const doc = formatAgentOpenApi(manifest, [ { id: "lead.submit", input: inputSchema, output: outputSchema }, ]); const op = doc.paths["/api/agent/v1/actions/lead.submit"]?.post; expect(op).toBeTruthy(); expect(op!.operationId).toBe("action.lead.submit"); expect(op!.summary).toBe("Anfrage senden"); expect(doc.components.schemas["lead.submit-input"]).toEqual(inputSchema); expect(doc.components.schemas["lead.submit-output"]).toEqual(outputSchema); expect(op!.requestBody!.content["application/json"].schema).toEqual(inputSchema); // RFC-1112: Idempotency-Key header parameter. const idemParam = op!.parameters!.find((p) => p.name === "Idempotency-Key"); expect(idemParam).toBeTruthy(); expect(idemParam!.in).toBe("header"); expect(idemParam!.required).toBe(false); // RFC-1112: problem+json error responses + shared problem-details schema. expect(doc.components.schemas["problem-details"]).toBeTruthy(); for (const status of ["400", "409", "413", "422", "429", "502"]) { const res = op!.responses[status]; expect(res, `missing ${status} response`).toBeTruthy(); expect(res!.content?.["application/problem+json"]).toBeTruthy(); } }); test('formatAgentOpenApi: sideEffect "none" action exposes the readiness envelope and no 422 (RFC-1169 AC-4)', () => { const inputSchema = { type: "object" as const, additionalProperties: false as const, required: ["message"], properties: { message: { type: "string" as const } }, }; const outputSchema: CapabilityOutputSchema = { type: "object", additionalProperties: false, required: ["ready"], properties: { ready: { type: "boolean" }, draftId: { type: "string" }, missing: { type: "array", items: { type: "string" } }, problems: { type: "array" }, summary: { type: "object" }, nextAction: { type: "string" }, sideEffect: { type: "string" }, }, }; const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, actions: [ { id: "lead.prepare", url: "/api/agent/v1/actions/lead.prepare", title: { de: "Anfrage prüfen" }, inputSchemaRef: "#/components/schemas/lead.prepare-input", entitlement: "agent.actions", sideEffect: "none", }, ], }); const doc = formatAgentOpenApi(manifest, [ { id: "lead.prepare", input: inputSchema, output: outputSchema, sideEffect: "none" }, ]); const op = doc.paths["/api/agent/v1/actions/lead.prepare"]?.post; expect(op).toBeTruthy(); const out = doc.components.schemas["lead.prepare-output"] as typeof outputSchema; for (const field of ["ready", "draftId", "missing", "summary", "nextAction", "sideEffect"]) { expect(out.properties[field as keyof typeof out.properties], `missing ${field}`).toBeTruthy(); } // RFC-1169: preview validation failures answer 200 ready:false — the 422 must // not be declared (agents would otherwise treat it as the failure contract). expect(op!.responses["422"]).toBe(undefined); expect(op!.responses["400"]).toBeTruthy(); // invalid JSON still applies }); test("formatAgentOpenApi: an action ref without a matching capability schema is skipped defensively", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, actions: [ { id: "lead.submit", url: "/api/agent/v1/actions/lead.submit", title: { de: "x" }, inputSchemaRef: "#/components/schemas/lead.submit-input", entitlement: "agent.actions", }, ], }); const doc = formatAgentOpenApi(manifest, []); expect(doc.paths["/api/agent/v1/actions/lead.submit"]).toBe(undefined); }); test("formatAgentOpenApi: mcp interface present → x-gogol-mcp is set", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, mcp: { url: "/api/agent/mcp", protocolVersion: "2025-06-18" }, }); const doc = formatAgentOpenApi(manifest, []); expect(doc["x-gogol-mcp"]).toEqual({ url: "/api/agent/mcp", protocolVersion: "2025-06-18" }); }); test("formatAgentOpenApi: search interface present → /api/agent/search GET path with dynamic lang enum", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de", "uk"] }, search: { url: "/api/agent/search", model: "@cf/baai/bge-m3", dimensions: 1024 }, }); const doc = formatAgentOpenApi(manifest, []); const op = doc.paths["/api/agent/search"]?.get; expect(op).toBeTruthy(); expect(op!.operationId).toBe("search.query"); expect(op!.tags).toEqual(["search"]); expect(doc.components.schemas["search-response"]).toBeTruthy(); const langParam = op!.parameters!.find((p: { name: string }) => p.name === "lang"); expect(langParam).toBeTruthy(); expect(langParam!.schema.enum).toEqual(["de", "uk"]); }); test("formatAgentOpenApi: search interface absent → no search path", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, }); const doc = formatAgentOpenApi(manifest, []); expect(doc.paths["/api/agent/search"]).toBe(undefined); expect(doc.components.schemas["search-response"]).toBe(undefined); }); test("formatAgentOpenApi: agentGuidance renders into the operation description (RFC-1115 AC-4)", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, actions: [ { id: "lead.submit", url: "/api/agent/v1/actions/lead.submit", title: { de: "Anfrage senden" }, inputSchemaRef: "#/components/schemas/lead.submit-input", entitlement: "agent.actions", }, ], }); const doc = formatAgentOpenApi(manifest, [ { id: "lead.submit", input: { type: "object" }, output: { type: "object" as const, additionalProperties: false as const, properties: {} }, description: { de: "Reicht eine Kontaktanfrage ein." }, agentGuidance: { sideEffect: "write", whenToUse: "ein Besucher das Gewerbe kontaktieren will.", whenNotToUse: "die Antwort bereits auf der Seite steht.", retrySemantics: "mit demselben Idempotency-Key sicher wiederholbar.", }, }, ]); const op = doc.paths["/api/agent/v1/actions/lead.submit"]?.post; expect(op).toBeTruthy(); expect(op!.description).toContain("Reicht eine Kontaktanfrage ein."); expect(op!.description).toContain("Use this when ein Besucher das Gewerbe kontaktieren will."); expect(op!.description).toContain( "Do not use this when die Antwort bereits auf der Seite steht.", ); expect(op!.description).toContain("side effects"); expect(op!.description).toContain( "Retry semantics: mit demselben Idempotency-Key sicher wiederholbar.", ); }); test("formatAgentOpenApi: absent agentGuidance falls back to the base description (RFC-1115 AC-5)", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, actions: [ { id: "lead.submit", url: "/api/agent/v1/actions/lead.submit", title: { de: "Anfrage senden" }, inputSchemaRef: "#/components/schemas/lead.submit-input", entitlement: "agent.actions", }, ], }); const doc = formatAgentOpenApi(manifest, [ { id: "lead.submit", input: { type: "object" }, output: { type: "object" as const, additionalProperties: false as const, properties: {} }, description: { de: "Reicht eine Kontaktanfrage ein." }, }, ]); const op = doc.paths["/api/agent/v1/actions/lead.submit"]?.post; expect(op!.description).toBe("Reicht eine Kontaktanfrage ein."); }); test("formatAgentOpenApi: search-response schema matches the flattened citation shape (RFC-1115 AC-1)", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, search: { url: "/api/agent/search", model: "@cf/baai/bge-m3", dimensions: 1024 }, }); const doc = formatAgentOpenApi(manifest, []); const schema = doc.components.schemas["search-response"] as { properties: { results: { items: { required: string[]; properties: Record } } }; }; const item = schema.properties.results.items; expect(item.required).toEqual([ "title", "canonicalUrl", "updatedAt", "lang", "type", "score", "text", ]); expect(item.properties.canonicalUrl).toEqual({ type: "string" }); expect(item.properties.chunk).toBeUndefined(); }); // --------------------------------------------------------------------------- // RFC-1171: deprecated alias operations (AC-5) // --------------------------------------------------------------------------- test("formatAgentOpenApi: deprecations[] project deprecated alias ops with successor pointer (AC-5)", () => { const manifest = buildAgentSurfaceManifest({ generatedAt: "2026-01-01T00:00:00.000Z", site: "s", baseUrl: "https://s.example", languages: { default: "de", supported: ["de"] }, actions: [ { id: "lead.submit", url: "/api/agent/v1/actions/lead.submit", title: { de: "Anfrage senden" }, inputSchemaRef: "#/components/schemas/lead.submit-input", entitlement: "agent.actions", }, ], }); const doc = formatAgentOpenApi(manifest, [ { id: "lead.submit", input: { type: "object" as const, properties: {} }, output: { type: "object" as const, properties: {} }, }, ]); // Canonical operation stays untouched. const canonical = doc.paths["/api/agent/v1/actions/lead.submit"]?.post; expect(canonical?.deprecated).toBeUndefined(); expect(canonical?.operationId).toBe("action.lead.submit"); // Alias operation exists, is deprecated, and names its successor. const alias = doc.paths["/api/agent/actions/lead.submit"]?.post; expect(alias?.deprecated).toBe(true); expect(alias?.operationId).toBe("action.lead.submit.deprecated"); expect(alias?.description).toContain("/api/agent/v1/actions/lead.submit"); });