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).
80 lines
3.3 KiB
JavaScript
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, []);
|
|
});
|