/*
RFC-1165 NLWeb /ask — unit tests for the framework-free core (nlweb-ask.ts).
Determinism (AC-2), honest prefer failures (AC-3), no-answer contract (AC-4),
accept-language gating + fallback (AC-9), site-mismatch, schema_object mirror
sourcing — all with stubbed ports; no astro/cloudflare imports.
- No HTTP/adapter tests — createAgentNlwebRoute lives in astro.ts.
- RFC-1311: Compass header sweep — declares the nlweb suite's contract for test-file scaffold coverage.
*/
import { describe, expect, it } from "vitest";
import type {
AgentSurfaceManifest,
SearchChunk,
SearchManifest,
} from "@warpgogol/werkstatt-shared/agent";
import {
NLWEB_MIN_SCORE,
NLWEB_VERSION,
noAnswerSentence,
nlwebQueryId,
parseNlwebRequest,
resolveAcceptLanguage,
runNlwebAsk,
type NlwebAskPorts,
} from "./nlweb-ask.ts";
const manifest = {
site: "warpgogol",
baseUrl: "https://warpgogol.com",
languages: { default: "de", supported: ["de", "uk"] },
} as AgentSurfaceManifest;
const chunks: SearchChunk[] = [
{
id: "de:/leistungen/:b1",
url: "/leistungen/",
lang: "de",
type: "page",
blockId: "b1",
heading: "Leistungen",
text: "Wir bauen digitale Fundamente für Freiburger Unternehmen.",
title: "Leistungen",
canonicalUrl: "https://warpgogol.com/leistungen/",
updatedAt: "2026-09-01",
},
{
id: "uk:/posluhy/:b1",
url: "/uk/posluhy/",
lang: "uk",
type: "page",
blockId: "b1",
heading: "Послуги",
text: "Ми будуємо цифровий фундамент для компаній Фрайбурга.",
title: "Послуги",
canonicalUrl: "https://warpgogol.com/uk/posluhy/",
updatedAt: "2026-09-01",
},
];
const searchManifest = { chunks } as SearchManifest;
const mirrorDoc = { "@graph": [{ "@type": "WebPage", name: "Leistungen" }] };
function ports(
matches: Array<{ id: string; score: number }>,
mirrors: Record = {},
): NlwebAskPorts {
return {
ai: { run: async () => ({ data: [[0.1, 0.2, 0.3]] }) },
searchIndex: { query: async () => ({ matches }) },
fetchMirror: async (p) => mirrors[p] ?? null,
};
}
const goodBody = {
query: { text: "Was baut ihr?" },
prefer: { streaming: false, response_format: "conversational_search", mode: "list" },
};
// ---------------------------------------------------------------------------
// AC-2 — determinism
// ---------------------------------------------------------------------------
describe("runNlwebAsk determinism (AC-2)", () => {
it("returns byte-identical bodies for identical requests", async () => {
const p = ports([{ id: "de:/leistungen/:b1", score: 0.9 }], { "leistungen.jsonld": mirrorDoc });
const a = await runNlwebAsk(goodBody, manifest, searchManifest, p);
const b = await runNlwebAsk(goodBody, manifest, searchManifest, p);
expect(JSON.stringify(a.body)).toBe(JSON.stringify(b.body));
expect(a.body._meta.query_id).toBe(b.body._meta.query_id);
expect(a.body._meta.version).toBe(NLWEB_VERSION);
expect(a.body._meta.response_type).toBe("answer");
});
it("nlwebQueryId is stable and site-scoped", async () => {
const id1 = await nlwebQueryId("warpgogol", "de", "Was baut ihr?");
const id2 = await nlwebQueryId("warpgogol", "de", "Was baut ihr?");
const other = await nlwebQueryId("other-site", "de", "Was baut ihr?");
expect(id1).toBe(id2);
expect(id1).not.toBe(other);
expect(id1).toMatch(/^[0-9a-f]{16}$/);
});
});
// ---------------------------------------------------------------------------
// AC-3 — honest prefer failures
// ---------------------------------------------------------------------------
describe("parseNlwebRequest honest failures (AC-3)", () => {
it.each([
[{ prefer: { streaming: true } }, "unsupported-streaming"],
[{ prefer: { response_format: "rss" } }, "unsupported-response_format"],
[{ prefer: { mode: "generate" } }, "unsupported-mode"],
[{ query: { text: "a" } }, "invalid-query-text"],
[{ query: { text: "x".repeat(501) } }, "invalid-query-text"],
[{ query: { text: "Was?", site: "other-site" } }, "site-mismatch"],
])("rejects %j with reason %s", (patch, reason) => {
const body = { query: { text: "Was baut ihr?" }, ...patch };
const r = parseNlwebRequest(body, "warpgogol");
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.status).toBe(400);
expect(r.reason).toBe(reason);
}
});
it("returns failure response_type on bad prefer via runNlwebAsk", async () => {
const r = await runNlwebAsk(
{ query: { text: "Was baut ihr?" }, prefer: { streaming: true } },
manifest,
searchManifest,
ports([]),
);
expect(r.status).toBe(400);
expect(r.body._meta.response_type).toBe("failure");
expect(r.body.reason).toBe("unsupported-streaming");
expect(r.body.total_results).toBe(0);
});
});
// ---------------------------------------------------------------------------
// AC-4 — no-answer contract
// ---------------------------------------------------------------------------
describe("no-answer contract (AC-4)", () => {
it("below-threshold retrieval returns the deterministic no-answer", async () => {
const r = await runNlwebAsk(
goodBody,
manifest,
searchManifest,
ports([{ id: "de:/leistungen/:b1", score: NLWEB_MIN_SCORE - 0.01 }]),
);
expect(r.status).toBe(200);
expect(r.body.total_results).toBe(0);
expect(r.body.results).toEqual([]);
expect(r.body.answer).toBe(noAnswerSentence("de"));
expect(r.body.content).toBeUndefined();
});
it("uk accept-language returns the uk no-answer sentence", async () => {
const r = await runNlwebAsk(
{ query: { text: "Що ви будуєте?" }, prefer: { "accept-language": "uk" } },
manifest,
searchManifest,
ports([]),
);
expect(r.body.answer).toBe(noAnswerSentence("uk"));
expect(r.body._meta.lang).toBe("uk");
});
});
// ---------------------------------------------------------------------------
// AC-9 — accept-language gating
// ---------------------------------------------------------------------------
describe("accept-language (AC-9)", () => {
it("uk language selects uk chunks", async () => {
const r = await runNlwebAsk(
{ query: { text: "Що?" }, prefer: { "accept-language": "uk" } },
manifest,
searchManifest,
ports([
{ id: "de:/leistungen/:b1", score: 0.9 },
{ id: "uk:/posluhy/:b1", score: 0.8 },
]),
);
expect(r.body.results).toHaveLength(1);
expect(r.body.results[0].url).toBe("https://warpgogol.com/uk/posluhy/");
});
it("falls back to default-language chunks when the resolved lang has none", async () => {
const r = await runNlwebAsk(
{ query: { text: "Що?" }, prefer: { "accept-language": "uk" } },
manifest,
searchManifest,
ports([{ id: "de:/leistungen/:b1", score: 0.9 }]),
);
expect(r.body.results).toHaveLength(1);
expect(r.body.results[0].url).toBe("https://warpgogol.com/leistungen/");
});
it("resolveAcceptLanguage: q-sort, prefix match, default fallback", () => {
const s = ["de", "uk"];
expect(resolveAcceptLanguage("uk-UA;q=0.9,de;q=0.8", s, "de")).toBe("uk");
expect(resolveAcceptLanguage("de;q=0.9,uk;q=0.8", s, "de")).toBe("de");
expect(resolveAcceptLanguage("fr", s, "de")).toBe("de");
expect(resolveAcceptLanguage(undefined, s, "de")).toBe("de");
expect(resolveAcceptLanguage("", s, "de")).toBe("de");
});
});
// ---------------------------------------------------------------------------
// schema_object grounding (RFC-1164 mirrors)
// ---------------------------------------------------------------------------
describe("schema_object mirror grounding", () => {
it("attaches the mirror @graph when the mirror exists", async () => {
const r = await runNlwebAsk(
goodBody,
manifest,
searchManifest,
ports([{ id: "de:/leistungen/:b1", score: 0.9 }], { "leistungen.jsonld": mirrorDoc }),
);
expect(r.body.results[0].schema_object?.["@type"]).toBe("WebPage");
expect(r.body.results[0].schema_object?.["@graph"]).toEqual(mirrorDoc["@graph"]);
});
it("omits schema_object when the mirror is absent", async () => {
const r = await runNlwebAsk(
goodBody,
manifest,
searchManifest,
ports([{ id: "de:/leistungen/:b1", score: 0.9 }]),
);
expect(r.body.results[0].schema_object).toBeUndefined();
expect(r.body.results[0]["@type"]).toBe("WebPage");
});
});