/*
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");
});