ms-ai-architect/tests/kb-update/test-transform.test.mjs
Kjell Tore Guttormsen a583599ab1 feat(ms-ai-architect): R20 — Category-backfill på 25 kategorisløse ref-filer via insertMetaField [skip-docs]
Ny testet primitiv transform.insertMetaField (anker + 500B-back-off som insertHeaderFields, idempotent på eksakt label, body byte-identisk; 6 tester) + backfill-category.mjs (deterministisk folder->category-regel, hard per-fil-invariant, insert-only, aborterer for skriving ved avvik).

Fordeling: 14 Solution Architecture & Advisory (architecture/), 6 Microsoft AI Platforms (platforms/+development/), 4 Responsible AI & Governance, 1 MLOps & GenAIOps. Label = engelsk Category (322 vs 42 Kategori). Eksisterende recommended-mcp-servers/rag-maturity-model urort.

Roadmap: R20 done + R21 (Status/Last-updated not-due) encoded. Verifisering: 0 kategorislose filer (var 25); diff +25/-0; idempotent re-run; suite 764/764 exit 0.
2026-07-06 07:39:54 +02:00

943 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/kb-update/test-transform.test.mjs
// TDD for lag 4 — TRANSFORMASJON (doc → KB-fil). lib/transform.mjs is a pure
// helper library (mirrors decisions-io / verify-out): it builds the deterministic
// KB-file header, validates the mandatory-field contract, routes the file to its
// owning skill via the taxonomy (lag 0), and constructs the lag-5 `change` object.
// It NEVER writes — applying the file is the operator gate's job (commands/kb-update.md).
//
// Why Source lives in the HEADER block: build-registry / kb-headers.parseSourceHeader
// scan only the top 500 bytes, so a source buried in a bottom section (as the old
// prompt-template did) is invisible → the 0% authority_source coverage. Putting
// **Source:** in the header is what lets lag 3 backfill authority_source and lag-5
// rule 3 (authority mismatch) eventually fire.
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 {
buildKbHeader,
validateKbFile,
buildChange,
resolveTargetPath,
buildToc,
composeKbFile,
insertToc,
insertHeaderFields,
insertMetaField,
insertVerifiedFields,
normalizeStaleVerified,
stampVerifiedMeta,
} from '../../scripts/kb-update/lib/transform.mjs';
import {
parseSourceHeader,
parseTypeHeader,
parseVerifiedHeader,
parseVerifiedByHeader,
} from '../../scripts/kb-update/lib/kb-headers.mjs';
import { classifyChange } from '../../scripts/kb-update/lib/verify-out.mjs';
import { loadTaxonomy } from '../../scripts/kb-update/lib/taxonomy.mjs';
// The N4 (TOC-in-large-files) check is the cross-subsystem best-practice contract.
// Fase 1c births files that satisfy it; we assert parity against the REAL check so
// a future drift in N4's definition fails this test rather than silently diverging.
import { checkN4 } from '../../scripts/kb-eval/eval.mjs';
// A body long enough that header+body exceeds N4's 100-line "large file" threshold,
// carrying real ## section headings the TOC must pick up.
const LARGE_BODY =
'## Introduksjon\n\nNoe innledning.\n\n' +
'## Kjernekomponenter\n\n' +
'Mye innhold ...\n'.repeat(110) +
'\n## Beslutningsveiledning\n\nVelg X når ...\n';
const SHORT_BODY = '## Introduksjon\n\nKort.\n';
const __dirname = dirname(fileURLToPath(import.meta.url));
// A born-verified reference meta: Spor 3 Port 1 contract adds type/verified/verified_by
// (reference files are "born verified" — stamped only after the v2 judge clears them).
const META = {
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',
};
// --- buildKbHeader: mandatory fields, parseable by the real header scanners ----
test('buildKbHeader emits title, Last updated, Status, Source, Category in the header block', () => {
const h = buildKbHeader(META);
assert.match(h, /^# Azure AI Search - Hybrid Retrieval/m);
assert.match(h, /\*\*Last updated:\*\*\s*2026-06/);
assert.match(h, /\*\*Status:\*\*\s*GA/);
assert.match(h, /\*\*Category:\*\*\s*rag-architecture/);
assert.match(h, /\*\*Source:\*\*\s*https:\/\/learn\.microsoft\.com/);
});
test('buildKbHeader Source is within the top-500-byte region kb-headers scans', () => {
const h = buildKbHeader(META);
// The whole point: parseSourceHeader (top 500 bytes) must find it.
assert.equal(parseSourceHeader(h), META.source);
});
test('buildKbHeader throws when a mandatory field is missing (status-felt obligatorisk)', () => {
assert.throws(() => buildKbHeader({ ...META, status: undefined }), /status/i);
assert.throws(() => buildKbHeader({ ...META, source: '' }), /source/i);
assert.throws(() => buildKbHeader({ ...META, title: null }), /title/i);
assert.throws(() => buildKbHeader({ ...META, lastUpdated: undefined }), /last.?updated/i);
});
// --- Spor 3 Port 1/Port 2: type-aware create-guard contract --------------------
// Every reference file is "born verified": carries Type + Verified + Verified by in
// the header, all within the top-500-byte region the scanners read.
test('buildKbHeader emits Type, Verified, Verified by in the header (Port 1 contract)', () => {
const h = buildKbHeader(META);
assert.match(h, /\*\*Type:\*\*\s*reference/);
assert.match(h, /\*\*Verified:\*\*\s*2026-06-29/);
assert.match(h, /\*\*Verified by:\*\*\s*judge-v2/);
});
test('buildKbHeader: the new fields are parseable in the top-500-byte region', () => {
const h = buildKbHeader(META);
assert.equal(parseTypeHeader(h), 'reference');
assert.equal(parseVerifiedHeader(h), '2026-06-29');
assert.equal(parseVerifiedByHeader(h), 'judge-v2');
});
test('buildKbHeader: Verified by (the last header line) stays inside the 500-byte scan region for a realistic long title + URL', () => {
// Verified by sits after Source (the long field) — guard that a realistic worst case
// still fits, so a valid file is never wrongly flagged as missing verified_by.
const h = buildKbHeader({
title: 'Azure AI Foundry Model Router and Deployment Strategies for Enterprise',
status: 'Public Preview',
category: 'azure-ai-services',
type: 'reference',
source: 'https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/model-router-deployment-strategies-overview',
lastUpdated: '2026-06',
verified: '2026-06-29',
verified_by: 'judge-v2',
});
assert.equal(parseVerifiedByHeader(h), 'judge-v2', 'verified_by fell outside the 500-byte header region');
});
test('buildKbHeader: a reference file MUST carry verified + verified_by (born-verified guard)', () => {
assert.throws(() => buildKbHeader({ ...META, verified: undefined }), /verified/i);
assert.throws(() => buildKbHeader({ ...META, verified_by: '' }), /verified_by|verified by/i);
});
test('buildKbHeader: type defaults to reference when omitted (back-compat for callers)', () => {
const { type, ...noType } = META;
const h = buildKbHeader(noType);
assert.equal(parseTypeHeader(h), 'reference');
});
test('buildKbHeader: an unknown type is rejected', () => {
assert.throws(() => buildKbHeader({ ...META, type: 'blogpost' }), /type/i);
});
test('buildKbHeader: a non-reference file (methodology) needs NO source/verified', () => {
// Template/methodology/regulatory are out of correctness-scope — they carry no MS source.
const h = buildKbHeader({
title: 'ROS-metodikk (NS 5814)',
status: 'Stable',
category: 'norwegian-public-sector-ai-governance',
type: 'methodology',
lastUpdated: '2026-06',
});
assert.match(h, /\*\*Type:\*\*\s*methodology/);
assert.doesNotMatch(h, /\*\*Source:\*\*/);
assert.doesNotMatch(h, /\*\*Verified:\*\*/);
});
test('buildKbHeader: a non-reference file may NOT carry a Microsoft Learn source (contract)', () => {
assert.throws(
() => buildKbHeader({
title: 'X', status: 'Stable', category: 'c', type: 'template',
lastUpdated: '2026-06', source: 'https://learn.microsoft.com/azure/x',
}),
/source/i,
);
});
// --- validateKbFile: type-aware enforcement of the full contract ---------------
test('validateKbFile: a reference file missing verified/verified_by is invalid', () => {
// Header has Source but no Verified / Verified by → not born-verified → rejected.
const file =
'# 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';
const r = validateKbFile(file);
assert.equal(r.valid, false);
assert.ok(r.missing.includes('verified'), `expected 'verified' in ${r.missing}`);
assert.ok(r.missing.includes('verified_by'), `expected 'verified_by' in ${r.missing}`);
});
test('validateKbFile: a complete born-verified reference file is valid', () => {
const file = buildKbHeader(META) + '\n## Introduksjon\n\nNoe innhold.\n';
assert.equal(validateKbFile(file).valid, true);
});
test('validateKbFile: a methodology file without source/verified is valid (out of scope)', () => {
const file =
'# ROS-metodikk\n\n**Last updated:** 2026-06\n**Status:** Stable\n**Type:** methodology\n\n' +
'---\n\n## Metode\n\nNS 5814.\n';
const r = validateKbFile(file);
assert.equal(r.valid, true, `unexpected missing: ${r.missing}`);
});
// --- stampVerifiedMeta: the born-verified gate (judge clears → stamp; else refuse) --
// transform.mjs never calls the judge (pure lib). The command runs the v2 claim judge,
// aggregates per-claim results into {pass}, and calls this. A failing verdict throws —
// "nytt innhold gjennom judgen FØR commit; ellers ingen fil." (§4 Port 2.)
test('stampVerifiedMeta stamps verified=today + verified_by=judge-vN on a passing verdict', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
const m = stampVerifiedMeta(base, { pass: true }, '2026-06-29');
assert.equal(m.type, 'reference');
assert.equal(m.verified, '2026-06-29');
assert.equal(m.verified_by, 'judge-v3.1'); // current GATE-PASS judge (v3.1 adopted G1)
});
test('stampVerifiedMeta honours an explicit judgeVersion', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
assert.equal(stampVerifiedMeta(base, { pass: true, judgeVersion: 3 }, '2026-06-29').verified_by, 'judge-v3');
});
// --- G2: version-label generalisation (formatJudgeVersion via stampVerifiedMeta) -----
// The stamp guard now honours a minor-bearing label ('3.2' → judge-v3.2), preserves the
// integer path (regression above), defaults on absence, and THROWS on a malformed label
// rather than silently falling back — a mislabel would be a provenance lie in a public file.
test('stampVerifiedMeta honours a minor-bearing string judgeVersion override', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
assert.equal(
stampVerifiedMeta(base, { pass: true, judgeVersion: '3.2' }, '2026-06-29').verified_by,
'judge-v3.2',
);
});
test('stampVerifiedMeta honours a float judgeVersion override', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
assert.equal(
stampVerifiedMeta(base, { pass: true, judgeVersion: 3.2 }, '2026-06-29').verified_by,
'judge-v3.2',
);
});
test('stampVerifiedMeta defaults verified_by to judge-v3.1 when judgeVersion is absent', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
assert.equal(
stampVerifiedMeta(base, { pass: true }, '2026-06-29').verified_by,
'judge-v3.1',
);
});
test('stampVerifiedMeta throws on a malformed judgeVersion label (no silent fallback)', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
for (const bad of ['3.2.1', '', 'v3', '3.2 x']) {
assert.throws(
() => stampVerifiedMeta(base, { pass: true, judgeVersion: bad }, '2026-06-29'),
/judge version|malformed/i,
`expected throw for judgeVersion=${JSON.stringify(bad)}`,
);
}
});
test('stampVerifiedMeta → composeKbFile round-trips a minor-bearing judge version (end-to-end)', () => {
const base = { title: 'Azure AI Search', status: 'GA', category: 'rag-architecture',
source: 'https://learn.microsoft.com/azure/search/x', lastUpdated: '2026-06' };
const stamped = stampVerifiedMeta(base, { pass: true, judgeVersion: '3.2' }, '2026-06-29');
const file = composeKbFile(stamped, '## Introduksjon\n\nKort.\n');
assert.equal(parseVerifiedByHeader(file), 'judge-v3.2');
});
test('stampVerifiedMeta supports a human verification path', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
assert.equal(stampVerifiedMeta(base, { pass: true, by: 'human' }, '2026-06-29').verified_by, 'human');
});
test('stampVerifiedMeta REFUSES to stamp when the judge verdict is not passing', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
assert.throws(() => stampVerifiedMeta(base, { pass: false }, '2026-06-29'), /judge|verdict|verified/i);
assert.throws(() => stampVerifiedMeta(base, null, '2026-06-29'), /judge|verdict|verified/i);
});
test('stampVerifiedMeta rejects an invalid today date', () => {
const base = { title: 'T', status: 'GA', category: 'c',
source: 'https://learn.microsoft.com/x', lastUpdated: '2026-06' };
assert.throws(() => stampVerifiedMeta(base, { pass: true }, 'i går'), /date/i);
});
test('stampVerifiedMeta → composeKbFile is a complete born-verified file (end-to-end)', () => {
const base = { title: 'Azure AI Search', status: 'GA', category: 'rag-architecture',
source: 'https://learn.microsoft.com/azure/search/x', lastUpdated: '2026-06' };
const stamped = stampVerifiedMeta(base, { pass: true }, '2026-06-29');
const file = composeKbFile(stamped, '## Introduksjon\n\nKort.\n');
assert.equal(validateKbFile(file).valid, true);
assert.equal(parseVerifiedHeader(file), '2026-06-29');
assert.equal(parseVerifiedByHeader(file), 'judge-v3.1'); // default stamp parses back (dotted minor survives)
});
// --- validateKbFile: catches files missing the mandatory contract -------------
test('validateKbFile accepts a header-conformant file', () => {
const file = buildKbHeader(META) + '\n## Introduksjon\n\nNoe innhold.\n';
const r = validateKbFile(file);
assert.equal(r.valid, true);
assert.deepEqual(r.missing, []);
});
test('validateKbFile reports each missing mandatory field', () => {
// No Status, no Source, no Last updated, no title.
const r = validateKbFile('Bare brødtekst uten header.\n');
assert.equal(r.valid, false);
for (const f of ['title', 'last_updated', 'status', 'source']) {
assert.ok(r.missing.includes(f), `expected '${f}' in missing: ${r.missing}`);
}
});
test('validateKbFile flags a file with body-only source (the 0%-coverage failure mode)', () => {
// Source in a bottom section, NOT the header → invisible to top-500-byte scan.
const file =
'# Tittel\n\n**Last updated:** 2026-06\n**Status:** Preview\n\n---\n\n' +
'Mye innhold ...\n'.repeat(40) +
'\n## Kilder\n\n**Source:** https://learn.microsoft.com/x\n';
const r = validateKbFile(file);
assert.equal(r.valid, false);
assert.ok(r.missing.includes('source'));
});
// --- buildChange: the lag-4 → lag-5 contract ----------------------------------
test('buildChange produces a classifyChange-compatible object with safe defaults', () => {
const c = buildChange({
field: 'status',
oldValue: 'Preview',
newValue: 'GA',
sourceUrl: META.source,
});
assert.equal(c.field, 'status');
assert.equal(c.old_value, 'Preview');
assert.equal(c.new_value, 'GA');
assert.equal(c.source_url, META.source);
assert.equal(c.authority_source, null); // pre-lag-3 default
assert.deepEqual(c.refutations, []);
});
test('buildChange output flows through lag 5 — a status change is flagged, never auto-applied', () => {
const c = buildChange({ field: 'status', oldValue: 'Preview', newValue: 'GA', sourceUrl: META.source });
const verdict = classifyChange(c);
assert.equal(verdict.verdict, 'flagged');
assert.equal(verdict.status_claim, true);
});
test('buildChange output flows through lag 5 — a non-status change can auto-apply', () => {
const c = buildChange({
field: 'description',
oldValue: 'En kort beskrivelse.',
newValue: 'En litt lengre, oppdatert beskrivelse.',
sourceUrl: META.source,
});
assert.equal(classifyChange(c).verdict, 'auto-applied');
});
// --- resolveTargetPath: taxonomy-driven routing (replaces category-skill-map) --
test('resolveTargetPath routes via the taxonomy category_skill (disk-truth)', () => {
const tax = loadTaxonomy();
assert.equal(
resolveTargetPath(tax, 'rag-architecture', 'hybrid-retrieval.md'),
'skills/ms-ai-engineering/references/rag-architecture/hybrid-retrieval.md',
);
// a former divergence — taxonomy says advisor, not engineering
assert.equal(
resolveTargetPath(tax, 'prompt-engineering', 'few-shot.md'),
'skills/ms-ai-advisor/references/prompt-engineering/few-shot.md',
);
});
test('resolveTargetPath returns null for an unknown category (caller must gate)', () => {
const tax = loadTaxonomy();
assert.equal(resolveTargetPath(tax, 'does-not-exist', 'x.md'), null);
});
// --- buildToc: deterministic TOC from the body's ## section headings -----------
test('buildToc emits a ## Innhold heading with anchor links for each ## section', () => {
const toc = buildToc('## Introduksjon\n\ntekst\n\n## Kostnad og lisensiering\n\ntekst\n');
assert.match(toc, /^##\s+Innhold/m);
assert.match(toc, /- \[Introduksjon\]\(#introduksjon\)/);
assert.match(toc, /- \[Kostnad og lisensiering\]\(#kostnad-og-lisensiering\)/);
});
test('buildToc slugifies GitHub-style: lowercase, punctuation stripped, spaces→hyphens', () => {
const toc = buildToc('## 1. Introduksjon & Oversikt\n\ntekst\n');
assert.match(toc, /- \[1\. Introduksjon & Oversikt\]\(#1-introduksjon--oversikt\)/);
});
test('buildToc returns empty string when the body has no ## sections', () => {
assert.equal(buildToc('Bare brødtekst, ingen overskrifter.\n'), '');
assert.equal(buildToc('# Bare en H1\n\ntekst\n'), ''); // H1 is the header title, not a section
});
test('buildToc never lists the Innhold heading itself (idempotent re-runs)', () => {
const body = '## Innhold\n\n- [Introduksjon](#introduksjon)\n\n## Introduksjon\n\ntekst\n';
const toc = buildToc(body);
// exactly one anchor — to Introduksjon — never an anchor back to Innhold
assert.doesNotMatch(toc, /\(#innhold\)/);
assert.match(toc, /\(#introduksjon\)/);
});
// --- composeKbFile: the deterministic generator (header + TOC-if-large + body) --
test('composeKbFile births a large file with canonical header AND a TOC', () => {
const file = composeKbFile(META, LARGE_BODY);
assert.match(file, /^# Azure AI Search - Hybrid Retrieval/m); // canonical header first
assert.equal(parseSourceHeader(file), META.source); // Source in top-500-byte region
assert.match(file, /^##\s+Innhold/m); // TOC present
assert.equal(validateKbFile(file).valid, true);
});
test('composeKbFile output for a large file passes the REAL N4 check (anti-regression oracle)', () => {
const file = composeKbFile(META, LARGE_BODY);
const n4 = checkN4([{ path: 'skills/x/references/y/z.md', content: file }]);
assert.equal(n4.largeFiles, 1);
assert.equal(n4.withToc, 1);
assert.equal(n4.pass, true);
});
test('composeKbFile does NOT add a TOC to a small file (TOC is for large files only)', () => {
const file = composeKbFile(META, SHORT_BODY);
assert.doesNotMatch(file, /^##\s+Innhold/m);
assert.equal(validateKbFile(file).valid, true);
});
test('composeKbFile is idempotent: a body that already carries a TOC is not double-TOCd', () => {
const once = composeKbFile(META, LARGE_BODY);
const body = once.slice(once.indexOf('---\n') + 4); // strip header, keep TOC+body
const twice = composeKbFile(META, body);
assert.equal((twice.match(/^##\s+Innhold/gm) || []).length, 1);
assert.equal(validateKbFile(twice).valid, true);
});
// --- insertToc: backfill a TOC into an EXISTING large file (Fase 1b) -----------
// composeKbFile builds a NEW file from meta+body; insertToc retrofits the ~20 largest
// hand-written files on disk, whose legacy headers vary (some carry a `---` rule, some
// don't), without touching their bodies. It places `## Innhold` immediately before the
// first non-fenced `## ` section — the same relative slot composeKbFile uses.
// Large legacy file WITHOUT a `---` header rule (the secure-model-deployment shape).
const LEGACY_NO_RULE =
'# Secure Model Deployment\n\n' +
'**Kategori:** AI Security Engineering\n' +
'**Dato:** 2026-02-05\n\n' +
'## Introduksjon\n\n' + 'Brødtekst.\n'.repeat(110) +
'\n## Container Scanning\n\nMer tekst.\n' +
'\n## Avslutning\n\nSlutt.\n';
// Large legacy file WITH a `---` header rule (the ros-ai-threat-library shape).
const LEGACY_WITH_RULE =
'# AI-trusselbibliotek\n\n' +
'**Sist oppdatert:** 2026-06-18\n' +
'**Kategori:** Norwegian Public Sector AI Governance\n\n' +
'---\n\n' +
'## Oversikt\n\n' + 'Brødtekst.\n'.repeat(110) +
'\n## Metode\n\nMer tekst.\n';
test('insertToc adds a ## Innhold before the first ## section, body untouched (no `---` header)', () => {
const out = insertToc(LEGACY_NO_RULE);
assert.equal((out.match(/^##\s+Innhold/gm) || []).length, 1);
// TOC sits before the first content section …
assert.ok(out.indexOf('## Innhold') < out.indexOf('## Introduksjon'));
// … and lists every ## section in order
assert.match(out, /- \[Introduksjon\]\(#introduksjon\)/);
assert.match(out, /- \[Container Scanning\]\(#container-scanning\)/);
assert.match(out, /- \[Avslutning\]\(#avslutning\)/);
// body from the first section onward is byte-identical (zero churn)
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(LEGACY_NO_RULE));
});
test('insertToc places the TOC after a `---` header rule, before the first section', () => {
const out = insertToc(LEGACY_WITH_RULE);
assert.ok(out.indexOf('---') < out.indexOf('## Innhold'));
assert.ok(out.indexOf('## Innhold') < out.indexOf('## Oversikt'));
const tail = (s) => s.slice(s.indexOf('## Oversikt'));
assert.equal(tail(out), tail(LEGACY_WITH_RULE)); // body preserved
});
test('insertToc is a no-op on a small file (TOC is for large files only)', () => {
const small = '# T\n\n## A\n\ntekst\n\n## B\n\nmer\n';
assert.equal(insertToc(small), small);
});
test('insertToc is idempotent: a file that already carries a TOC is unchanged', () => {
const once = insertToc(LEGACY_NO_RULE);
const twice = insertToc(once);
assert.equal(twice, once);
assert.equal((once.match(/^##\s+Innhold/gm) || []).length, 1);
});
test('insertToc turns an N4-failing large file into an N4-passing one (real-check oracle)', () => {
const before = checkN4([{ path: 'skills/x/references/y/z.md', content: LEGACY_NO_RULE }]);
assert.equal(before.largeFiles, 1);
assert.equal(before.withToc, 0);
assert.equal(before.pass, false);
const after = checkN4([{ path: 'skills/x/references/y/z.md', content: insertToc(LEGACY_NO_RULE) }]);
assert.equal(after.withToc, 1);
assert.equal(after.pass, true);
});
test('insertToc is fence-aware: a ## inside a code fence is neither anchor nor listed', () => {
const fenced =
'# T\n\n**Kategori:** X\n\n' +
'```md\n## Not A Real Section\n```\n\n' +
'## Faktisk Seksjon\n\n' + 'Brødtekst.\n'.repeat(110) +
'\n## Andre Seksjon\n\nx.\n';
const out = insertToc(fenced);
assert.doesNotMatch(out, /\[Not A Real Section\]\(#/); // fenced heading not listed
assert.match(out, /- \[Faktisk Seksjon\]\(#faktisk-seksjon\)/);
assert.match(out, /- \[Andre Seksjon\]\(#andre-seksjon\)/);
// the fenced block stays above the TOC; TOC sits before the first real section
assert.ok(out.indexOf('## Not A Real Section') < out.indexOf('## Innhold'));
assert.ok(out.indexOf('## Innhold') < out.indexOf('## Faktisk Seksjon'));
});
// --- validateKbFile: large files must carry a TOC (the write-path regression gate)
test('validateKbFile flags a large file missing a TOC', () => {
const file = buildKbHeader(META) + '\n' + 'En linje med innhold.\n'.repeat(120);
const r = validateKbFile(file);
assert.equal(r.valid, false);
assert.ok(r.missing.includes('toc'), `expected 'toc' in missing: ${r.missing}`);
});
test('validateKbFile accepts a large file that carries a TOC', () => {
const file = composeKbFile(META, LARGE_BODY);
const r = validateKbFile(file);
assert.equal(r.valid, true);
assert.ok(!r.missing.includes('toc'));
});
test('validateKbFile does NOT require a TOC for a small file', () => {
const file = buildKbHeader(META) + '\n## Introduksjon\n\nKort innhold.\n';
const r = validateKbFile(file);
assert.equal(r.valid, true);
assert.ok(!r.missing.includes('toc'));
});
// --- ARCHITECTURE INVARIANT: transform.mjs is a pure lib, imports NO write-utils
test('transform.mjs imports NO write-utils (pure transformer, never writes)', () => {
const src = readFileSync(join(__dirname, '../../scripts/kb-update/lib/transform.mjs'), 'utf8');
const importLines = src.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n');
assert.doesNotMatch(importLines, /\bsaveDecisions\b/);
assert.doesNotMatch(importLines, /\bsaveRegistry\b/);
assert.doesNotMatch(importLines, /\bsaveReport\b/);
assert.doesNotMatch(importLines, /from\s+'[^']*atomic-write[^']*'/);
assert.doesNotMatch(importLines, /from\s+'[^']*\/backup\.mjs'/);
});
// --- insertHeaderFields: Spor 1 migration primitive (backfill Type/Source) -------
// Unlike buildKbHeader (which emits a COMPLETE new header), insertHeaderFields inserts
// only the missing Port-1 field(s) into an EXISTING legacy file's top-region meta block,
// immediately after the last line of the first contiguous run of bold-label meta lines
// (language-agnostic across the 3 dialects), never before the first `## ` section, never
// past the 500-byte scan window. Body byte-identical; per-field idempotent.
// Dialect A — **Category:** (English labels), with a `---` header rule.
const HDR_CATEGORY =
'# MLOps Fundamentals\n\n' +
'**Last updated:** 2026-06-19\n' +
'**Status:** GA\n' +
'**Category:** MLOps & GenAIOps\n\n' +
'---\n\n' +
'## Introduksjon\n\nBrødtekst.\n';
// Dialect B — **Kategori:** (Norwegian labels), with a `---` header rule.
const HDR_KATEGORI =
'# Feedback Loops\n\n' +
'**Kategori:** MLOps & GenAIOps\n' +
'**Sist oppdatert:** 2026-06\n\n' +
'---\n\n' +
'## Introduksjon\n\nBrødtekst.\n';
// Dialect C — pipe-delimited meta row, NO `---` rule (the ai-center-of-excellence shape).
const HDR_PIPE =
'# AI Center of Excellence\n\n' +
'**Kategori:** Responsible AI & Governance\n' +
'**Opprettet:** 2026-04 | **Sist oppdatert:** 2026-06-19\n' +
'**Confidence:** HIGH (Microsoft CAF)\n\n' +
'## Introduksjon\n\nBrødtekst.\n';
// A large file (>100 lines) with a category header — for the combined insertToc oracle.
const HDR_LARGE =
'# Big Ref\n\n' +
'**Last updated:** 2026-06\n' +
'**Status:** GA\n' +
'**Category:** X\n\n' +
'---\n\n' +
LARGE_BODY;
const MS_URL = 'https://learn.microsoft.com/azure/machine-learning/concept-mlops';
test('insertHeaderFields inserts Type+Source after the meta block (category dialect), body byte-identical', () => {
const out = insertHeaderFields(HDR_CATEGORY, { type: 'reference', source: MS_URL });
assert.equal(parseTypeHeader(out), 'reference');
assert.equal(parseSourceHeader(out), MS_URL);
// Type/Source sit inside the meta block: after **Category:**, before the `---` rule.
assert.ok(out.indexOf('**Category:**') < out.indexOf('**Type:**'));
assert.ok(out.indexOf('**Type:**') < out.indexOf('\n---'));
// body (from the first ## section) is byte-identical
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_CATEGORY));
});
test('insertHeaderFields works on the Norwegian **Kategori:** dialect, body byte-identical', () => {
const out = insertHeaderFields(HDR_KATEGORI, { type: 'reference', source: MS_URL });
assert.equal(parseTypeHeader(out), 'reference');
assert.equal(parseSourceHeader(out), MS_URL);
assert.ok(out.indexOf('**Sist oppdatert:**') < out.indexOf('**Type:**'));
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_KATEGORI));
});
test('insertHeaderFields works on the pipe-delimited dialect without a `---` rule, body byte-identical', () => {
const out = insertHeaderFields(HDR_PIPE, { type: 'reference', source: MS_URL });
assert.equal(parseTypeHeader(out), 'reference');
assert.equal(parseSourceHeader(out), MS_URL);
// inserted after the last meta line (**Confidence:**), before the first ## section
assert.ok(out.indexOf('**Confidence:**') < out.indexOf('**Type:**'));
assert.ok(out.indexOf('**Type:**') < out.indexOf('## Introduksjon'));
// the pipe meta row is untouched (siblings preserved)
assert.match(out, /\*\*Opprettet:\*\* 2026-04 \| \*\*Sist oppdatert:\*\* 2026-06-19/);
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_PIPE));
});
// Dialect A' — a meta line (Status) whose single line is itself >500 bytes (real corpus:
// agentic-rag-patterns.md, gpt5-gpt41-pricing-models.md pack a paragraph into **Status:**).
// The anchor must back off to the last meta line that still leaves room in the 500-byte
// window, so the inserted Type/Source stay parseable — never anchor PAST the giant line.
const HDR_GIANT_META =
'# Agentic RAG Patterns — Agent-styrt retrieval\n\n' +
'**Last updated:** 2026-06-19\n' +
'**Status:** ' + 'GA; '.repeat(140) + 'slutt\n' + // single meta line ~560 bytes
'**Category:** RAG Architecture\n\n' +
'---\n\n' +
'## Introduksjon\n\nBrødtekst.\n';
test('insertHeaderFields keeps Type+Source inside the 500-byte window when a meta line is itself >500B (anchor backs off, does not overshoot)', () => {
// Guard: the fixture really does have a Status line that alone crosses the window.
assert.ok(HDR_GIANT_META.indexOf('**Category:**') > 500, 'fixture must push Category past 500B');
const out = insertHeaderFields(HDR_GIANT_META, { type: 'reference', source: MS_URL });
// The whole point: both fields must survive the top-500-byte scan.
assert.equal(parseTypeHeader(out), 'reference', 'Type fell outside the 500-byte window');
assert.equal(parseSourceHeader(out), MS_URL, 'Source fell outside the 500-byte window');
// They landed BEFORE the giant Status line (after Last updated), not after it.
assert.ok(out.indexOf('**Last updated:**') < out.indexOf('**Type:**'));
assert.ok(out.indexOf('**Type:**') < out.indexOf('**Status:**'));
// body (from the first ## section) is byte-identical
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_GIANT_META));
});
test('insertHeaderFields for a non-reference type emits Type only, never Source', () => {
const out = insertHeaderFields(HDR_KATEGORI, { type: 'methodology', source: MS_URL });
assert.equal(parseTypeHeader(out), 'methodology');
assert.equal(parseSourceHeader(out), null);
assert.doesNotMatch(out, /\*\*Source:\*\*/);
});
test('insertHeaderFields is per-field idempotent (a re-run inserts nothing)', () => {
const once = insertHeaderFields(HDR_CATEGORY, { type: 'reference', source: MS_URL });
const twice = insertHeaderFields(once, { type: 'reference', source: MS_URL });
assert.equal(twice, once);
assert.equal((once.match(/\*\*Type:\*\*/g) || []).length, 1);
assert.equal((once.match(/\*\*Source:\*\*/g) || []).length, 1);
});
test('insertHeaderFields adds only the missing field when Type is already present', () => {
const hasType =
'# T\n\n**Status:** GA\n**Category:** X\n**Type:** reference\n\n---\n\n## A\n\ntekst\n';
const out = insertHeaderFields(hasType, { type: 'reference', source: MS_URL });
assert.equal((out.match(/\*\*Type:\*\*/g) || []).length, 1); // not duplicated
assert.equal(parseSourceHeader(out), MS_URL); // Source backfilled
});
test('insertHeaderFields keeps an existing **Source:** (the 7 pre-sourced files)', () => {
const hasSource =
'# T\n\n**Status:** GA\n**Type:** reference\n' +
'**Source:** https://learn.microsoft.com/existing\n\n---\n\n## A\n\ntekst\n';
const out = insertHeaderFields(hasSource, { type: 'reference', source: MS_URL });
assert.equal(parseSourceHeader(out), 'https://learn.microsoft.com/existing');
assert.equal((out.match(/\*\*Source:\*\*/g) || []).length, 1);
});
test('insertHeaderFields rejects an unknown type', () => {
assert.throws(() => insertHeaderFields(HDR_CATEGORY, { type: 'blogpost' }), /type/i);
});
// --- insertMetaField: the R20/R21 base-field backfill primitive ------------------
// Inserts ONE bold-label meta line (**<Label>:** <value>) into an EXISTING file that
// lacks it, using the same anchor + 500-byte back-off discipline as insertHeaderFields
// (last line of the first contiguous meta run that fits the window; title fallback for
// the "None" dialect). Body byte-identical; idempotent on the exact label. Serves R20
// (**Category:**) and R21 (**Status:**, **Last updated:**).
// A file with a meta run but NO **Category:** (the R20 target shape).
const NOCAT =
'# Decision Trees\n\n' +
'**Last updated:** 2026-06-19\n' +
'**Status:** Established Practice\n\n' +
'---\n\n' +
'## Introduksjon\n\nBrødtekst.\n';
// The "None" dialect: a platform ref with NO bold-label meta line at all.
const NOMETA =
'# Azure AI Foundry\n\n' +
'Foundry er Microsofts plattform.\n\n' +
'## Oversikt\n\nBrødtekst.\n';
test('insertMetaField inserts **Category:** after the meta run, before the rule, body byte-identical', () => {
const out = insertMetaField(NOCAT, 'Category', 'Solution Architecture & Advisory');
assert.match(out, /\*\*Category:\*\* Solution Architecture & Advisory/);
// lands inside the meta run: after Status, before the `---` rule
assert.ok(out.indexOf('**Status:**') < out.indexOf('**Category:**'));
assert.ok(out.indexOf('**Category:**') < out.indexOf('\n---'));
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(NOCAT));
});
test('insertMetaField anchors on the title for the "None" dialect (no meta lines), before the first ## section', () => {
const out = insertMetaField(NOMETA, 'Category', 'Microsoft AI Platforms');
assert.match(out, /\*\*Category:\*\* Microsoft AI Platforms/);
assert.ok(out.indexOf('# Azure AI Foundry') < out.indexOf('**Category:**'));
assert.ok(out.indexOf('**Category:**') < out.indexOf('## Oversikt'));
const tail = (s) => s.slice(s.indexOf('## Oversikt'));
assert.equal(tail(out), tail(NOMETA));
});
test('insertMetaField is idempotent on the exact label (a re-run inserts nothing)', () => {
const once = insertMetaField(NOCAT, 'Category', 'Solution Architecture & Advisory');
const twice = insertMetaField(once, 'Category', 'Solution Architecture & Advisory');
assert.equal(twice, once);
assert.equal((once.match(/\*\*Category:\*\*/g) || []).length, 1);
});
test('insertMetaField keeps the field inside the 500-byte window when a meta line is itself >500B (anchor backs off)', () => {
const giant =
'# Big\n\n' +
'**Status:** ' + 'GA; '.repeat(140) + 'slutt\n\n' + // single meta line ~560 bytes
'---\n\n' +
'## A\n\ntekst\n';
const out = insertMetaField(giant, 'Category', 'X');
// backed off to the title anchor — Category lands BEFORE the giant Status line, in-window
assert.ok(out.indexOf('# Big') < out.indexOf('**Category:**'));
assert.ok(out.indexOf('**Category:**') < out.indexOf('**Status:**'));
assert.ok(out.indexOf('**Category:**') < 500, 'Category fell outside the 500-byte window');
const tail = (s) => s.slice(s.indexOf('## A'));
assert.equal(tail(out), tail(giant));
});
test('insertMetaField preserves a pipe-delimited meta row (siblings untouched)', () => {
const out = insertMetaField(HDR_PIPE, 'Type', 'reference');
assert.match(out, /\*\*Opprettet:\*\* 2026-04 \| \*\*Sist oppdatert:\*\* 2026-06-19/);
assert.ok(out.indexOf('**Confidence:**') < out.indexOf('**Type:**'));
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_PIPE));
});
test('insertMetaField rejects a blank label or blank value', () => {
assert.throws(() => insertMetaField(NOCAT, '', 'x'), /label/i);
assert.throws(() => insertMetaField(NOCAT, 'Category', ' '), /value/i);
});
test('insertToc(insertHeaderFields(large)) → Type+Source+TOC, passes real checkN4, body byte-identical except header + one ## Innhold', () => {
const withHdr = insertHeaderFields(HDR_LARGE, { type: 'reference', source: MS_URL });
const out = insertToc(withHdr);
assert.equal(parseTypeHeader(out), 'reference');
assert.equal(parseSourceHeader(out), MS_URL);
const n4 = checkN4([{ path: 'skills/x/references/y/z.md', content: out }]);
assert.equal(n4.largeFiles, 1);
assert.equal(n4.withToc, 1);
assert.equal(n4.pass, true);
assert.equal((out.match(/^##\s+Innhold/gm) || []).length, 1);
// body from the first real section is byte-identical to the original body
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_LARGE));
});
// --- insertVerifiedFields: the R7R10 surgical stamp primitive ---------------------
// Insert-or-update ONLY the **Verified:** / **Verified by:** header lines in place, body
// byte-identical, NEVER a TOC (contrast composeKbFile, which rebuilds the header + may add
// ## Innhold → body diff). THROWS when the stamp would land past the 500-byte window so a
// judge-cleared file is never given a silent, unparseable stamp.
// A migrated reference carrying Type+Source but NOT yet Verified — the R7 stamp target.
const HDR_UNVERIFIED =
'# Azure AI Foundry\n\n' +
'**Last updated:** 2026-06\n' +
'**Status:** GA\n' +
'**Category:** AI Services\n' +
'**Type:** reference\n' +
'**Source:** https://learn.microsoft.com/azure/ai-foundry/x\n\n' +
'---\n\n' +
'## Introduksjon\n\nBrødtekst.\n';
// Same, but large (>100 lines) — proves stamping never triggers a TOC insertion.
const HDR_LARGE_UNVERIFIED =
'# Big Ref\n\n' +
'**Last updated:** 2026-06\n' +
'**Status:** GA\n' +
'**Category:** X\n' +
'**Type:** reference\n' +
'**Source:** ' + MS_URL + '\n\n' +
'---\n\n' +
LARGE_BODY;
// A header whose title line alone exceeds the 500-byte window: any stamp inserted after it
// lands past the window → invisible to parseVerified*Header → insertVerifiedFields must THROW.
const HDR_NO_ROOM =
'# ' + 'A'.repeat(520) + '\n\n' +
'**Type:** reference\n**Source:** ' + MS_URL + '\n\n---\n\n## Introduksjon\n\ntekst.\n';
test('insertVerifiedFields stamps Verified + Verified by, body byte-identical, no TOC', () => {
const out = insertVerifiedFields(HDR_UNVERIFIED, { verified: '2026-07-04', verified_by: 'judge-v3.1' });
assert.equal(parseVerifiedHeader(out), '2026-07-04');
assert.equal(parseVerifiedByHeader(out), 'judge-v3.1');
assert.doesNotMatch(out, /## Innhold/); // never a TOC (contrast composeKbFile)
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_UNVERIFIED)); // body byte-identical
});
test('insertVerifiedFields updates an existing stamp in place (idempotent + re-stampable)', () => {
const once = insertVerifiedFields(HDR_UNVERIFIED, { verified: '2026-07-04', verified_by: 'judge-v3.1' });
const twice = insertVerifiedFields(once, { verified: '2026-07-04', verified_by: 'judge-v3.1' });
assert.equal(twice, once, 'same values → byte-identical (idempotent)');
const updated = insertVerifiedFields(once, { verified: '2026-08-01', verified_by: 'human' });
assert.equal(parseVerifiedHeader(updated), '2026-08-01');
assert.equal(parseVerifiedByHeader(updated), 'human');
assert.equal((updated.match(/\*\*Verified:\*\*/g) || []).length, 1, 'not duplicated');
assert.equal((updated.match(/\*\*Verified by:\*\*/g) || []).length, 1, 'not duplicated');
});
test('insertVerifiedFields never inserts a TOC into a large file (contrast composeKbFile)', () => {
const out = insertVerifiedFields(HDR_LARGE_UNVERIFIED, { verified: '2026-07-04', verified_by: 'judge-v3.1' });
assert.equal(parseVerifiedHeader(out), '2026-07-04');
assert.doesNotMatch(out, /## Innhold/);
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(HDR_LARGE_UNVERIFIED));
});
test('insertVerifiedFields throws when the stamp would land past the 500-byte window', () => {
assert.throws(
() => insertVerifiedFields(HDR_NO_ROOM, { verified: '2026-07-04', verified_by: 'judge-v3.1' }),
/500|window|vindu/i,
);
});
test('insertVerifiedFields requires both verified and verified_by', () => {
assert.throws(() => insertVerifiedFields(HDR_UNVERIFIED, { verified: '2026-07-04' }), /verified_by|required/i);
assert.throws(() => insertVerifiedFields(HDR_UNVERIFIED, { verified_by: 'judge-v3.1' }), /verified|required/i);
});
// --- normalizeStaleVerified: surgical carve-out for the 14 `**Verified:** MCP` files ---
// Acts on EVERY stale (non-date) **Verified:** that falls inside the 500-byte scan window —
// exactly what parseVerifiedHeader reads as the file's verified value, whether it sits in
// the header block OR as a stray duplicate just below the first `---` (a corpus artifact
// that still poisons the read). A clean YYYY-MM(-DD) date at the first position is left
// untouched, and anything past the 500-byte window is genuine body content and never
// touched. Pipe-safe: on a pipe row it removes only the Verified token + one adjacent ` | `,
// keeping siblings. Idempotent.
const VERIFIED_STALE =
'# T\n\n**Last updated:** 2026-06-19\n**Verified:** MCP 2026-06-19\n' +
'**Status:** GA\n**Category:** X\n\n---\n\n## A\n\ntekst.\n';
const VERIFIED_CLEAN =
'# T\n\n**Last updated:** 2026-06\n**Verified:** 2026-06\n**Status:** GA\n\n---\n\n## A\n\ntekst\n';
// The mlops-fundamentals-overview.md shape: a stale header Verified AND a duplicate
// below the `---` rule (line-10 shape). Both sit inside the 500-byte scan window, so both
// poison parseVerifiedHeader — the normalizer removes BOTH (the blessed body-dup carve-out).
const VERIFIED_BODY_DUP =
'# MLOps Fundamentals\n\n**Last updated:** 2026-06-19\n**Verified:** MCP 2026-06-19\n' +
'**Status:** GA\n**Category:** MLOps\n\n---\n\n**Verified:** MCP 2026-06-19\n\n' +
'## Introduksjon\n\ntekst.\n';
// The ai-center-of-excellence-setup.md shape: Verified is the last token of a
// pipe-delimited meta row.
const VERIFIED_PIPE =
'# AI Center of Excellence\n\n**Kategori:** Responsible AI & Governance\n' +
'**Opprettet:** 2026-04 | **Sist oppdatert:** 2026-06-19 | **Verified:** MCP 2026-06-19\n' +
'**Confidence:** HIGH (Microsoft CAF)\n\n## Introduksjon\n\ntekst.\n';
test('normalizeStaleVerified removes a standalone stale **Verified:** MCP header line (parseVerifiedHeader → null)', () => {
const out = normalizeStaleVerified(VERIFIED_STALE);
assert.equal(parseVerifiedHeader(out), null);
assert.doesNotMatch(out, /\*\*Verified:\*\*/);
// siblings preserved
assert.match(out, /\*\*Last updated:\*\* 2026-06-19/);
assert.match(out, /\*\*Status:\*\* GA/);
// body untouched
const tail = (s) => s.slice(s.indexOf('## A'));
assert.equal(tail(out), tail(VERIFIED_STALE));
});
test('normalizeStaleVerified leaves a clean YYYY-MM(-DD) Verified date untouched', () => {
assert.equal(normalizeStaleVerified(VERIFIED_CLEAN), VERIFIED_CLEAN);
assert.equal(parseVerifiedHeader(VERIFIED_CLEAN), '2026-06');
});
test('normalizeStaleVerified removes BOTH the header **Verified:** and the stray duplicate below --- within the 500B window', () => {
const out = normalizeStaleVerified(VERIFIED_BODY_DUP);
// both the header AND the body-dup are stale MCP within 500B → both gone
assert.equal((out.match(/\*\*Verified:\*\*/g) || []).length, 0);
assert.equal(parseVerifiedHeader(out), null, 'no stale Verified may survive in the scan window');
// header meta is intact
assert.match(out, /\*\*Last updated:\*\* 2026-06-19/);
assert.match(out, /\*\*Status:\*\* GA/);
// prose from ## Introduksjon is byte-identical — only the stray metadata line was removed
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(VERIFIED_BODY_DUP));
});
test('normalizeStaleVerified on a pipe row removes only the Verified token, preserving siblings', () => {
const out = normalizeStaleVerified(VERIFIED_PIPE);
assert.equal(parseVerifiedHeader(out), null);
assert.match(out, /\*\*Opprettet:\*\* 2026-04/);
assert.match(out, /\*\*Sist oppdatert:\*\* 2026-06-19/);
assert.doesNotMatch(out, /\*\*Verified:\*\*/);
// no dangling separator left at the row end
assert.doesNotMatch(out, /2026-06-19 \|\s*$/m);
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
assert.equal(tail(out), tail(VERIFIED_PIPE));
});
test('normalizeStaleVerified is idempotent', () => {
const once = normalizeStaleVerified(VERIFIED_STALE);
assert.equal(normalizeStaleVerified(once), once);
const oncePipe = normalizeStaleVerified(VERIFIED_PIPE);
assert.equal(normalizeStaleVerified(oncePipe), oncePipe);
const onceDup = normalizeStaleVerified(VERIFIED_BODY_DUP);
assert.equal(normalizeStaleVerified(onceDup), onceDup);
});
test('normalizeStaleVerified is a no-op when there is no header Verified', () => {
const clean = '# T\n\n**Status:** GA\n\n---\n\n## A\n\ntekst\n';
assert.equal(normalizeStaleVerified(clean), clean);
});