// tests/kb-update/test-migrate-apply.test.mjs // Integration tests for scripts/kb-update/migrate-corpus.mjs — the unified Spor 1 // applier (Step 7). Hermetic: every case runs against a fresh mkdtemp corpus via the // injected `--root`/`backupRoot`, so nothing ever writes to the real skills/ tree. // // Covers the Step-7 acceptance set: idempotency, advisor-exclusion, throw-guard, // atomic (no *.tmp.* leftover + import-grep), backup+restore round-trip, dry-run // default byte-identity, body-after-`---` identity + one `## Innhold`, post-write // parse assertion (500B truncation), and normalize→insert ordering. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, readdirSync, existsSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { planFileMutation, migrateCorpus, } from '../../scripts/kb-update/migrate-corpus.mjs'; import { parseTypeHeader, parseSourceHeader, parseVerifiedHeader, } from '../../scripts/kb-update/lib/kb-headers.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const SRC = join(HERE, '..', '..', 'scripts', 'kb-update', 'migrate-corpus.mjs'); function withTmp(fn) { const dir = mkdtempSync(join(tmpdir(), 'migrate-test-')); try { return fn(dir); } finally { rmSync(dir, { recursive: true, force: true }); } } /** Materialize a corpus under `root` from a {relpath: content} map. */ function makeCorpus(root, files) { for (const [rel, content] of Object.entries(files)) { const path = join(root, rel); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, content, 'utf8'); } } /** Read every file under `root` into a {relpath: content} map (posix keys). */ function readAll(root) { const out = {}; function walk(dir, prefix) { for (const entry of readdirSync(dir, { withFileTypes: true })) { const rel = prefix ? `${prefix}/${entry.name}` : entry.name; if (entry.isDirectory()) walk(join(dir, entry.name), rel); else if (entry.isFile()) out[rel] = readFileSync(join(dir, entry.name), 'utf8'); } } walk(root, ''); return out; } /** Canonical `skills/…` manifest key for a corpus-relative path. */ const key = (rel) => `skills/${rel}`; const REF_SMALL = `# Azure AI Foundry Overview **Category:** Engineering **Status:** Active --- ## Overview Foundry is a platform. ## Details More detail here. `; const LARGE_SECTIONS = Array.from({ length: 8 }, (_, i) => `## Section ${i + 1}\n\n${'body paragraph line.\n'.repeat(14)}`, ).join('\n'); const REF_LARGE = `# Big Reference **Category:** Engineering **Status:** Active --- ${LARGE_SECTIONS}`; const REF_STALE_VERIFIED = `# Stale Verified File **Category:** Engineering **Status:** Active **Verified:** MCP 2026-06 --- ## Overview Body. `; test('planFileMutation — reference entry stamps Type + Source; body after --- byte-identical', () => { const entry = { type: 'reference', source: 'https://learn.microsoft.com/azure/ai-foundry' }; const plan = planFileMutation(key('ms-ai-engineering/references/a.md'), REF_SMALL, entry); assert.equal(plan.changed, true); assert.equal(parseTypeHeader(plan.content), 'reference'); assert.equal(parseSourceHeader(plan.content), entry.source); // Body from the first `---` rule onward is untouched (header-only insertion; small file → no TOC). const marker = '\n---\n'; assert.equal(plan.content.slice(plan.content.indexOf(marker)), REF_SMALL.slice(REF_SMALL.indexOf(marker))); }); test('planFileMutation — deferred reference (no source) gets Type only, never Source', () => { const entry = { type: 'reference', deferred: true, deferReason: 'no-ms-citation' }; const plan = planFileMutation(key('ms-ai-governance/references/d.md'), REF_SMALL, entry); assert.equal(parseTypeHeader(plan.content), 'reference'); assert.equal(parseSourceHeader(plan.content), null); assert.equal(plan.expectSource, false); }); test('planFileMutation — throw-guard fires on an advisor target path', () => { const entry = { type: 'reference', source: 'https://learn.microsoft.com/x' }; assert.throws( () => planFileMutation('skills/ms-ai-advisor/references/architecture/cost-models.md', REF_SMALL, entry), /advisor/i, ); }); test('planFileMutation — normalize then insert applied in order on a **Verified:** MCP fixture', () => { const entry = { type: 'reference', source: 'https://learn.microsoft.com/azure/x' }; const plan = planFileMutation(key('ms-ai-security/references/v.md'), REF_STALE_VERIFIED, entry); // Stale header Verified stripped (normalize ran) AND Type inserted (insert ran). assert.equal(parseVerifiedHeader(plan.content), null); assert.equal(parseTypeHeader(plan.content), 'reference'); assert.equal(parseSourceHeader(plan.content), entry.source); }); test('planFileMutation — already-migrated content is a no-op (idempotent)', () => { const entry = { type: 'reference', source: 'https://learn.microsoft.com/azure/ai-foundry' }; const once = planFileMutation(key('ms-ai-engineering/references/a.md'), REF_SMALL, entry); const twice = planFileMutation(key('ms-ai-engineering/references/a.md'), once.content, entry); assert.equal(twice.changed, false); assert.equal(twice.content, once.content); }); test('migrateCorpus — dry-run (default) leaves the corpus byte-identical on disk', () => { withTmp((tmp) => { const root = join(tmp, 'skills'); makeCorpus(root, { 'ms-ai-engineering/references/a.md': REF_SMALL }); const before = readAll(root); const manifest = { [key('ms-ai-engineering/references/a.md')]: { type: 'reference', source: 'https://learn.microsoft.com/x' } }; const report = migrateCorpus({ root, backupRoot: join(tmp, '.kb-backup'), manifest, write: false }); assert.equal(report.write, false); assert.ok(report.counts.changed > 0, 'dry-run still reports the pending change'); assert.equal(report.backupPath, null); assert.deepEqual(readAll(root), before); }); }); test('migrateCorpus — --write stamps non-advisor files and leaves advisor byte-identical', () => { withTmp((tmp) => { const root = join(tmp, 'skills'); makeCorpus(root, { 'ms-ai-engineering/references/a.md': REF_SMALL, 'ms-ai-advisor/references/architecture/cost-models.md': REF_SMALL, }); const advisorBefore = readFileSync(join(root, 'ms-ai-advisor/references/architecture/cost-models.md'), 'utf8'); const manifest = { [key('ms-ai-engineering/references/a.md')]: { type: 'reference', source: 'https://learn.microsoft.com/x' }, // Advisor entry is present in the real manifest (deferred), but the walker must never reach it. [key('ms-ai-advisor/references/architecture/cost-models.md')]: { type: 'reference', deferred: true, deferReason: 'advisor-scope' }, }; const report = migrateCorpus({ root, backupRoot: join(tmp, '.kb-backup'), manifest, write: true }); assert.equal(report.counts.written, 1); // Non-advisor mutated. const eng = readFileSync(join(root, 'ms-ai-engineering/references/a.md'), 'utf8'); assert.equal(parseTypeHeader(eng), 'reference'); assert.equal(parseSourceHeader(eng), 'https://learn.microsoft.com/x'); // Advisor untouched — byte-identical. assert.equal(readFileSync(join(root, 'ms-ai-advisor/references/architecture/cost-models.md'), 'utf8'), advisorBefore); }); }); test('migrateCorpus — second --write run writes 0 files (idempotent on disk)', () => { withTmp((tmp) => { const root = join(tmp, 'skills'); makeCorpus(root, { 'ms-ai-engineering/references/a.md': REF_SMALL }); const manifest = { [key('ms-ai-engineering/references/a.md')]: { type: 'reference', source: 'https://learn.microsoft.com/x' } }; const opts = { root, backupRoot: join(tmp, '.kb-backup'), manifest, write: true }; const first = migrateCorpus(opts); assert.equal(first.counts.written, 1); const second = migrateCorpus(opts); assert.equal(second.counts.written, 0); assert.equal(second.counts.changed, 0); }); }); test('migrateCorpus — atomic write leaves no *.tmp.* leftover', () => { withTmp((tmp) => { const root = join(tmp, 'skills'); makeCorpus(root, { 'ms-ai-engineering/references/a.md': REF_SMALL }); const manifest = { [key('ms-ai-engineering/references/a.md')]: { type: 'reference', source: 'https://learn.microsoft.com/x' } }; migrateCorpus({ root, backupRoot: join(tmp, '.kb-backup'), manifest, write: true }); const names = Object.keys(readAll(root)); assert.equal(names.some((n) => /\.tmp\./.test(n)), false, 'no atomic-write temp file survived'); }); }); test('migrate-corpus source — uses atomicWriteSync, never bare writeFileSync', () => { const src = readFileSync(SRC, 'utf8'); assert.match(src, /atomicWriteSync/, 'imports/uses the atomic writer'); assert.doesNotMatch(src, /\bwriteFileSync\b/, 'never calls writeFileSync directly for corpus writes'); assert.match(src, /ms-ai-advisor/, 'carries the advisor fence'); }); test('migrateCorpus — backup exists and restore() round-trips byte-identity', () => { withTmp((tmp) => { const root = join(tmp, 'skills'); makeCorpus(root, { 'ms-ai-engineering/references/a.md': REF_SMALL, 'ms-ai-security/references/b.md': REF_STALE_VERIFIED, }); const original = readAll(root); const manifest = { [key('ms-ai-engineering/references/a.md')]: { type: 'reference', source: 'https://learn.microsoft.com/x' }, [key('ms-ai-security/references/b.md')]: { type: 'reference', source: 'https://learn.microsoft.com/y' }, }; const report = migrateCorpus({ root, backupRoot: join(tmp, '.kb-backup'), manifest, write: true }); assert.ok(report.backupPath && existsSync(report.backupPath), 'backup snapshot exists'); assert.equal(typeof report.restore, 'function'); // Corpus is mutated now… assert.notDeepEqual(readAll(root), original); // …restore brings it back byte-identical. report.restore(); assert.deepEqual(readAll(root), original); }); }); test('migrateCorpus — large file gains exactly one ## Innhold, sections preserved', () => { withTmp((tmp) => { const root = join(tmp, 'skills'); makeCorpus(root, { 'ms-ai-engineering/references/big.md': REF_LARGE }); const manifest = { [key('ms-ai-engineering/references/big.md')]: { type: 'reference', source: 'https://learn.microsoft.com/x' } }; migrateCorpus({ root, backupRoot: join(tmp, '.kb-backup'), manifest, write: true }); const out = readFileSync(join(root, 'ms-ai-engineering/references/big.md'), 'utf8'); assert.equal((out.match(/^## Innhold$/gm) || []).length, 1, 'exactly one TOC heading'); assert.equal(parseTypeHeader(out), 'reference'); // All original sections survive. for (let i = 1; i <= 8; i++) assert.match(out, new RegExp(`^## Section ${i}$`, 'm')); }); }); test('migrateCorpus — post-write parse assertion catches a deep-preamble (>500B) file', () => { withTmp((tmp) => { const root = join(tmp, 'skills'); // A title long enough that the inserted **Type:** line lands past the 500-byte // scan window → parseTypeHeader returns null on re-read → the applier must throw. const deep = `# ${'A'.repeat(490)}\n\n**Category:** Engineering\n\n## Section\n\nBody.\n`; makeCorpus(root, { 'ms-ai-engineering/references/deep.md': deep }); const original = readAll(root); const manifest = { [key('ms-ai-engineering/references/deep.md')]: { type: 'reference', source: 'https://learn.microsoft.com/x' } }; assert.throws( () => migrateCorpus({ root, backupRoot: join(tmp, '.kb-backup'), manifest, write: true }), /truncat|500|header/i, ); // The applier restored the corpus on the failed assertion — no partial mutation left behind. assert.deepEqual(readAll(root), original); }); });