126 lines
5.9 KiB
JavaScript
126 lines
5.9 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,
|
|
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');
|
|
});
|