/*
RFC-1165 / DNA-101: property-based tests for the pure helpers of the
NLWeb /ask core — invariants over arbitrary inputs, not hand-picked cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1165: created — PBT coverage for nlweb-ask pure helpers (fo-review DNA-101 follow-up).
*/
import { test, expect, describe } from "vitest";
import fc from "fast-check";
import {
nlwebMirrorPath,
nlwebQueryId,
noAnswerSentence,
parseNlwebRequest,
resolveAcceptLanguage,
} from "./nlweb-ask.ts";
const SUPPORTED = ["de", "uk", "en"];
const DEFAULT = "de";
describe("resolveAcceptLanguage — PBT", () => {
test("always resolves into supported ∪ {default} for arbitrary headers", () => {
fc.assert(
fc.property(fc.option(fc.string(), { nil: undefined }), (header) => {
const resolved = resolveAcceptLanguage(header, SUPPORTED, DEFAULT);
expect([...SUPPORTED, DEFAULT]).toContain(resolved);
}),
);
});
test("undefined and empty headers resolve to the default language", () => {
fc.assert(
fc.property(fc.constantFrom(undefined, "", " ", " , , q=9"), (header) => {
expect(resolveAcceptLanguage(header, SUPPORTED, DEFAULT)).toBe(DEFAULT);
}),
);
});
test("deterministic: same header resolves the same language", () => {
fc.assert(
fc.property(fc.option(fc.string(), { nil: undefined }), (header) => {
expect(resolveAcceptLanguage(header, SUPPORTED, DEFAULT)).toBe(
resolveAcceptLanguage(header, SUPPORTED, DEFAULT),
);
}),
);
});
test("a supported tag is honoured regardless of surrounding qualifiers", () => {
fc.assert(
fc.property(fc.constantFrom("uk", "uk-UA", "uk;q=0.9", "en;q=0.1,uk;q=0.7"), (header) => {
expect(resolveAcceptLanguage(header, SUPPORTED, DEFAULT)).toBe("uk");
}),
);
});
});
describe("nlwebMirrorPath — PBT", () => {
test("always ends in .jsonld and never starts with a slash", () => {
fc.assert(
fc.property(fc.string({ minLength: 0, maxLength: 40 }), (route) => {
const p = nlwebMirrorPath(route);
expect(p.endsWith(".jsonld")).toBe(true);
expect(p.startsWith("/")).toBe(false);
}),
);
});
test("deterministic and root-shaped inputs collapse to index.jsonld", () => {
fc.assert(
fc.property(fc.string({ minLength: 0, maxLength: 40 }), (route) => {
expect(nlwebMirrorPath(route)).toBe(nlwebMirrorPath(route));
}),
);
fc.assert(
fc.property(fc.array(fc.constant("/"), { minLength: 1, maxLength: 5 }), (slashes) => {
expect(nlwebMirrorPath(slashes.join(""))).toBe("index.jsonld");
}),
);
});
});
describe("parseNlwebRequest — PBT", () => {
test("never throws on arbitrary input; failures are always status 400", () => {
fc.assert(
fc.property(fc.anything(), (body) => {
const r = parseNlwebRequest(body, "warpgogol");
if (!r.ok) expect(r.status).toBe(400);
}),
);
});
test("valid text + matching/undefined site always parses ok and trims", () => {
fc.assert(
fc.property(
fc.string({ minLength: 2, maxLength: 200 }).filter((s) => s.trim().length >= 2),
fc.option(fc.constant("warpgogol"), { nil: undefined }),
(text, site) => {
const body = { query: { text: ` ${text} `, site }, prefer: { streaming: false } };
const r = parseNlwebRequest(body, "warpgogol");
expect(r.ok).toBe(true);
if (r.ok) expect(r.request.query.text).toBe(text.trim());
},
),
);
});
test("any mismatched site always fails with site-mismatch", () => {
fc.assert(
fc.property(
fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s !== "warpgogol"),
(site) => {
const r = parseNlwebRequest({ query: { text: "Was baut ihr?", site } }, "warpgogol");
expect(r.ok).toBe(false);
if (!r.ok) expect(r.reason).toBe("site-mismatch");
},
),
);
});
});
describe("noAnswerSentence — PBT", () => {
test("always returns a non-empty string for arbitrary lang tokens", () => {
fc.assert(
fc.property(fc.string({ minLength: 0, maxLength: 8 }), (lang) => {
const s = noAnswerSentence(lang);
expect(typeof s).toBe("string");
expect(s.length).toBeGreaterThan(0);
}),
);
});
test("deterministic; unknown languages fall back to the English sentence", () => {
fc.assert(
fc.property(fc.string({ minLength: 0, maxLength: 8 }), (lang) => {
expect(noAnswerSentence(lang)).toBe(noAnswerSentence(lang));
}),
);
expect(noAnswerSentence("xx")).toBe(noAnswerSentence("en"));
});
});
describe("nlwebQueryId — PBT", () => {
test("16-hex chars; deterministic per (site, lang, text); input-sensitive", async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 20 }),
fc.string({ minLength: 1, maxLength: 4 }),
fc.string({ minLength: 2, maxLength: 100 }),
async (site, lang, text) => {
const a = await nlwebQueryId(site, lang, text);
const b = await nlwebQueryId(site, lang, text);
expect(a).toBe(b);
expect(a).toMatch(/^[0-9a-f]{16}$/);
const changed = await nlwebQueryId(site, lang, `${text}!`);
expect(changed).not.toBe(a);
},
),
);
});
});