// tests/kb-update/test-authority.test.mjs // TDD for lag 3 — VERIFISERING-INN. lib/authority.mjs establishes the per-file // authority binding: the **Source:** header IS the designated authority for a // file's claims (operatør-valg 2026-06-19: header-as-truth). This is what feeds // verify-out rule 3 (authority mismatch) — the regression's root cause — and what // build-registry uses to populate the per-URL authority_source field. // // Pure lib (mirrors decisions-io / verify-out / transform): no write-util imports. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { resolveAuthority, collectDeclaredSources, markUrlAuthority, authorityCoverage, } from '../../scripts/kb-update/lib/authority.mjs'; import { classifyChange } from '../../scripts/kb-update/lib/verify-out.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const FILE_WITH_SOURCE = '# Tittel\n\n**Last updated:** 2026-06\n**Status:** GA\n' + '**Source:** https://learn.microsoft.com/azure/search/hybrid-search-overview\n\n---\n\n## Innhold\n'; // --- resolveAuthority: the **Source:** header is the per-file authority --------- test('resolveAuthority returns the declared **Source:** URL', () => { assert.equal( resolveAuthority(FILE_WITH_SOURCE), 'https://learn.microsoft.com/azure/search/hybrid-search-overview', ); }); test('resolveAuthority returns null when no Source header is present', () => { assert.equal(resolveAuthority('# Tittel\n\n**Status:** GA\n\n---\n'), null); }); test('resolveAuthority rejects a non-URL Source value (no guessed authority)', () => { // Verifiseringsplikt: a free-text "source" is not an authority binding. assert.equal(resolveAuthority('# T\n\n**Source:** se Microsoft-dokumentasjonen\n\n---\n'), null); }); test('resolveAuthority accepts a non-Microsoft authority (e.g. EU regulation)', () => { // Governance files may anchor on the regulation itself, not a learn.microsoft.com page. const c = '# T\n\n**Source:** https://eur-lex.europa.eu/eli/reg/2024/1689/oj\n\n---\n'; assert.equal(resolveAuthority(c), 'https://eur-lex.europa.eu/eli/reg/2024/1689/oj'); }); // --- collectDeclaredSources: normalized set for registry matching -------------- test('collectDeclaredSources returns the normalized MS-Learn sources declared across files', () => { const set = collectDeclaredSources([ FILE_WITH_SOURCE, '# B\n\n**Source:** https://learn.microsoft.com/azure/foundry/openai/how-to/batch-blob-storage\n\n---\n', '# C — no source\n\n**Status:** GA\n\n---\n', '# D — non-MS authority\n\n**Source:** https://eur-lex.europa.eu/x\n\n---\n', ]); assert.ok(set.has('https://learn.microsoft.com/azure/search/hybrid-search-overview')); assert.ok(set.has('https://learn.microsoft.com/azure/foundry/openai/how-to/batch-blob-storage')); // non-MS authority is not a tracked registry URL → excluded from the matching set assert.equal(set.size, 2); }); test('collectDeclaredSources normalizes (trailing slash / locale) to match registry keys', () => { const set = collectDeclaredSources([ '# T\n\n**Source:** https://learn.microsoft.com/en-us/azure/search/hybrid-search-overview/\n\n---\n', ]); assert.ok(set.has('https://learn.microsoft.com/azure/search/hybrid-search-overview')); }); // --- markUrlAuthority: a URL entry is an authority iff a file declares it ------- test('markUrlAuthority returns the URL when it is a declared authority, else null', () => { const declared = new Set(['https://learn.microsoft.com/azure/search/hybrid-search-overview']); assert.equal( markUrlAuthority('https://learn.microsoft.com/azure/search/hybrid-search-overview', declared), 'https://learn.microsoft.com/azure/search/hybrid-search-overview', ); assert.equal(markUrlAuthority('https://learn.microsoft.com/azure/other/page', declared), null); }); // --- authorityCoverage: the coverage metric (criterion: > 0%) ------------------ test('authorityCoverage counts entries with a non-null authority_source', () => { const registry = { urls: { 'https://learn.microsoft.com/a': { authority_source: 'https://learn.microsoft.com/a' }, 'https://learn.microsoft.com/b': { authority_source: null }, 'https://learn.microsoft.com/c': { authority_source: 'https://learn.microsoft.com/c' }, }, }; const cov = authorityCoverage(registry); assert.equal(cov.withAuthority, 2); assert.equal(cov.total, 3); assert.ok(cov.ratio > 0); }); // --- END-TO-END: header → authority → verify-out rule 3 (the criterion) -------- test('rule 3 fires end-to-end on a real authority: source_url ≠ header authority → flagged', () => { // A NON-status change so rules 1 (status-gate) and 2 (refutation) stay silent — // this isolates rule 3 and proves the header→authority→mismatch path works. const authority = resolveAuthority(FILE_WITH_SOURCE); const verdict = classifyChange({ field: 'description', old_value: 'En setning.', new_value: 'En annen setning.', source_url: 'https://learn.microsoft.com/azure/search/some-other-tangential-page', authority_source: authority, refutations: [], }); assert.equal(verdict.verdict, 'flagged'); assert.equal(verdict.status_claim, false); // not a status claim — rule 3 alone flagged it assert.ok(verdict.reasons.some((r) => /authority/i.test(r)), `expected an authority reason: ${verdict.reasons}`); }); test('rule 3 stays silent when source_url matches the header authority', () => { const authority = resolveAuthority(FILE_WITH_SOURCE); const verdict = classifyChange({ field: 'description', old_value: 'En setning.', new_value: 'En annen setning.', source_url: authority, // same as authority → no mismatch authority_source: authority, refutations: [], }); assert.equal(verdict.verdict, 'auto-applied'); }); // --- ARCHITECTURE INVARIANT: authority.mjs imports NO write-utils -------------- test('authority.mjs imports NO write-utils (pure, never writes)', () => { const src = readFileSync(join(__dirname, '../../scripts/kb-update/lib/authority.mjs'), 'utf8'); const importLines = src.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n'); assert.doesNotMatch(importLines, /\bsaveRegistry\b/); assert.doesNotMatch(importLines, /\bsaveDecisions\b/); assert.doesNotMatch(importLines, /from\s+'[^']*atomic-write[^']*'/); assert.doesNotMatch(importLines, /from\s+'[^']*\/backup\.mjs'/); });