// 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, insertHeaderFields, validateKbFile, } from '../../scripts/kb-update/lib/transform.mjs'; import { parseTypeHeader, parseSourceHeader } from '../../scripts/kb-update/lib/kb-headers.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, []); }); // --- Spor 1 acceptance: the residual criterion after the substrate migration --- // These generate the migrated file through the REAL Session-2 primitive // (insertHeaderFields), then assert the create-guard's residual — proving the // substrate closes the worklist-relevant fields (type+source) even where base // fields lag. `verified`/`verified_by` stay open by design (the judge fills them, // next brief), so the residual for a conformant file is EXACTLY those two. // A base-field-CONFORMANT legacy file: title + English **Last updated:** + **Status:**, // small (no TOC required). Migrated → Type + Source stamped, still no Verified. const LEGACY_CONFORMANT = '# Azure AI Foundry\n\n**Last updated:** 2026-06\n**Status:** GA\n\n---\n\n## Oversikt\n\ntekst\n'; // A base-field-INCOMPLETE legacy file: Norwegian **Sist oppdatert:** (RE_LAST_UPDATED is // English-only) + **Kategori:** but NO **Status:**. Migrated → Type + Source stamped. const LEGACY_INCOMPLETE = '# Copilot Studio\n\n**Kategori:** Styring\n**Sist oppdatert:** 2026-06\n\n---\n\n## Oversikt\n\ntekst\n'; test('acceptance — a migrated base-field-conformant reference has residual EXACTLY [verified, verified_by]', () => { const migrated = insertHeaderFields(LEGACY_CONFORMANT, { type: 'reference', source: 'https://learn.microsoft.com/azure/ai-foundry', }); assert.equal(parseTypeHeader(migrated), 'reference'); assert.equal(parseSourceHeader(migrated), 'https://learn.microsoft.com/azure/ai-foundry'); assert.deepEqual(validateKbFile(migrated).missing, ['verified', 'verified_by']); }); test('acceptance — a migrated base-field-incomplete reference has residual ⊆ the 4-set, with type+source present', () => { const migrated = insertHeaderFields(LEGACY_INCOMPLETE, { type: 'reference', source: 'https://learn.microsoft.com/power-platform/copilot', }); const { missing } = validateKbFile(migrated); const FOUR_SET = ['last_updated', 'status', 'verified', 'verified_by']; assert.ok(missing.every((m) => FOUR_SET.includes(m)), `residual ${JSON.stringify(missing)} ⊆ 4-set`); // The substrate DID complete the worklist-relevant fields — type + source are present, // never in the residual, even though base fields (Norwegian date / no Status) lag. assert.ok(!missing.includes('source')); assert.equal(parseTypeHeader(migrated), 'reference'); assert.equal(parseSourceHeader(migrated), 'https://learn.microsoft.com/power-platform/copilot'); });