// tests/kb-update/test-validate-kb-file.test.mjs // The executable create-guard (Spor 3 Port 2): scripts/kb-update/validate-kb-file.mjs // is the gate any generator calls AFTER writing a candidate file and BEFORE committing. // It wraps transform.validateKbFile over one or more paths and exits non-zero if any // file violates the contract (no source / not born-verified / large file without a TOC). // // validatePaths is a pure function with an injectable reader, so these tests never // touch disk — they feed synthetic content and assert the verdict + ok flag. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { validatePaths } from '../../scripts/kb-update/validate-kb-file.mjs'; import { buildKbHeader } from '../../scripts/kb-update/lib/transform.mjs'; const GOOD = buildKbHeader({ title: 'Azure AI Search - Hybrid Retrieval', status: 'GA', category: 'rag-architecture', type: 'reference', source: 'https://learn.microsoft.com/azure/search/hybrid-search-overview', lastUpdated: '2026-06', verified: '2026-06-29', verified_by: 'judge-v2', }) + '\n## Introduksjon\n\nNoe innhold.\n'; // A reference file with a Source but NOT born-verified (no Verified / Verified by). const NOT_VERIFIED = '# T\n\n**Last updated:** 2026-06\n**Status:** GA\n**Type:** reference\n' + '**Source:** https://learn.microsoft.com/x\n\n---\n\n## A\n\ntekst\n'; // A reference file with no Source header at all (the 0%-coverage failure mode). const NO_SOURCE = '# T\n\n**Last updated:** 2026-06\n**Status:** GA\n\n---\n\n## A\n\ntekst\n'; function reader(map) { return (p) => { if (!(p in map)) throw new Error(`unexpected read: ${p}`); return map[p]; }; } test('validatePaths — all files valid → ok:true', () => { const r = validatePaths(['a.md', 'b.md'], reader({ 'a.md': GOOD, 'b.md': GOOD })); assert.equal(r.ok, true); assert.equal(r.results.length, 2); assert.ok(r.results.every((x) => x.valid)); }); test('validatePaths — a file missing source fails the gate', () => { const r = validatePaths(['bad.md'], reader({ 'bad.md': NO_SOURCE })); assert.equal(r.ok, false); assert.equal(r.results[0].valid, false); assert.ok(r.results[0].missing.includes('source')); }); test('validatePaths — a reference file that is not born-verified fails the gate', () => { const r = validatePaths(['nv.md'], reader({ 'nv.md': NOT_VERIFIED })); assert.equal(r.ok, false); assert.ok(r.results[0].missing.includes('verified')); assert.ok(r.results[0].missing.includes('verified_by')); }); test('validatePaths — one bad file in a batch fails the whole gate', () => { const r = validatePaths(['ok.md', 'bad.md'], reader({ 'ok.md': GOOD, 'bad.md': NO_SOURCE })); assert.equal(r.ok, false); assert.equal(r.results.find((x) => x.path === 'ok.md').valid, true); assert.equal(r.results.find((x) => x.path === 'bad.md').valid, false); }); test('validatePaths — a path that cannot be read is reported as invalid, not thrown', () => { const r = validatePaths(['missing.md'], () => { throw new Error('ENOENT'); }); assert.equal(r.ok, false); assert.equal(r.results[0].valid, false); assert.ok(r.results[0].missing.some((m) => /read|enoent/i.test(m))); }); test('validatePaths — no paths → ok:true (nothing to gate)', () => { const r = validatePaths([], reader({})); assert.equal(r.ok, true); assert.deepEqual(r.results, []); });