ms-ai-architect/tests/kb-update/test-validate-kb-file.test.mjs
Kjell Tore Guttormsen 5a0ba1fc9e feat(ms-ai-architect): Spor 3 Port 2 — create-time-guard (born-verified contract) [skip-docs]
Type-aware create-guard for reference-filer (Spor 3 Port 1/Port 2):

- kb-headers.mjs: parseTypeHeader/parseVerifiedHeader/parseVerifiedByHeader
  (samme top-500-byte bold-label-skann som parseSourceHeader).
- transform.mjs: buildKbHeader er type-aware (reference krever
  source+verified+verified_by; non-reference kaster pa MS-Learn-source) +
  emitterer Type/Verified/Verified by; validateKbFile type-aware;
  stampVerifiedMeta = fodt-verifisert-gate (stempler KUN ved bestatt
  judge-verdikt, ellers kaster).
- validate-kb-file.mjs (ny): kjorbar gate generatorer kaller for commit
  (exit != 0 ved kontraktbrudd).
- generate-skills.md + kb-update.md + transform-prompt.md: wiret til
  fodt-verifisert kontrakt (header-felt + judge-steg + create-guard-gate).

Suite 586/586 (33 nye + 1 invariant). Plugin-validering 239/0/0.

Utenfor scope (flagget): generate-skills.sh legacy (sonnet/Cosmo/no-source),
Cosmo-persona i generatorene (S-Cosmo), korpus-migrering av 306 filer (Spor 1).
2026-06-29 10:36:21 +02:00

80 lines
3.3 KiB
JavaScript

// 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, []);
});