- backup.mjs restore(relPaths): SCOPED per-fil-rollback erstatter hel-tre rmSync(srcDir)+cpSync. Ruller kun tilbake kjøringens egne skriv; parallelle økters skriv til søsken-filer overlever; intet destruktivt rm→cp-vindu. Nyskapte filer slettes; prior bytes gjenopprettes atomisk. - migrate-corpus.mjs: returnert restore() er nå en null-arg closure bundet til kjøringens skrive- liste (public API uendret) → scoped. detectStaleRollback aborterer ved forrige krasj-sentinel; cleanupOldBackups pruner backups forbi retention etter vellykket batch. - 5 drivere (backfill-status/-category, dedup-plain-header, relabel-dato/-dialect): writeFileSync → atomicWriteSync (crash-safe tmp+rename) + recovery-kontrakt i header. - backfill-category.mjs: refaktorert importerbar (isMain-guard + run()/planCategoryBackfill/ categoryForFile/FOLDER_CATEGORY-eksporter, parameterisert root) → testbar uten scan-side-effekt. - Tester (+16): 6 scoped-restore (parallel-preservering, ny-fil-sletting, throws-guard) erstatter 2 hel-tre; test-backfill-category (frosset taxonomi + hermetisk plan); test-driver-atomic-writes (5 drivere); migrate stale-abort + cleanup-wiring. Suite 859→875 exit 0. validate-plugin 250/0.
93 lines
3.8 KiB
JavaScript
93 lines
3.8 KiB
JavaScript
// tests/kb-update/test-backfill-category.test.mjs
|
|
// TDD for the Category-backfill driver (R20). RX-OPS2 makes it importable (isMain
|
|
// guard + extracted run()/planCategoryBackfill) so its deterministic contract can be
|
|
// pinned without executing the corpus scan as an import side effect, and routes its
|
|
// writes through the crash-safe atomic writer. The pure, frozen contract:
|
|
// - FOLDER_CATEGORY: the 5 operator-approved folder→category entries (2026-07-06).
|
|
// - categoryForFile: immediate-parent-folder lookup; unknown folder → null.
|
|
// - planCategoryBackfill: reads a corpus root, plans insert-only Category lines behind
|
|
// the same one-line/byte-identical invariant as the sibling drivers; files that
|
|
// already carry Category are idempotently skipped; unknown folders are unmatched
|
|
// (never written).
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, dirname } from 'node:path';
|
|
import {
|
|
FOLDER_CATEGORY,
|
|
categoryForFile,
|
|
planCategoryBackfill,
|
|
} from '../../scripts/kb-update/backfill-category.mjs';
|
|
|
|
function withTmp(fn) {
|
|
const dir = mkdtempSync(join(tmpdir(), 'bcat-test-'));
|
|
try {
|
|
return fn(dir);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function writeFile(root, rel, content) {
|
|
const path = join(root, rel);
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, content, 'utf8');
|
|
}
|
|
|
|
test('FOLDER_CATEGORY — frozen operator-approved taxonomy (5 folders, 2026-07-06)', () => {
|
|
assert.deepEqual(FOLDER_CATEGORY, {
|
|
architecture: 'Solution Architecture & Advisory',
|
|
platforms: 'Microsoft AI Platforms',
|
|
development: 'Microsoft AI Platforms',
|
|
'responsible-ai': 'Responsible AI & Governance',
|
|
'mlops-genaiops': 'MLOps & GenAIOps',
|
|
});
|
|
});
|
|
|
|
test('categoryForFile — maps immediate-parent folder; unknown folder → null', () => {
|
|
assert.equal(categoryForFile('skills/x/references/architecture/foo.md'), 'Solution Architecture & Advisory');
|
|
assert.equal(categoryForFile('skills/x/references/mlops-genaiops/bar.md'), 'MLOps & GenAIOps');
|
|
assert.equal(categoryForFile('skills/x/references/responsible-ai/baz.md'), 'Responsible AI & Governance');
|
|
assert.equal(categoryForFile('skills/x/references/unknown-folder/qux.md'), null);
|
|
});
|
|
|
|
test('planCategoryBackfill — plans a Category-less file in a known folder (one line, right value)', () => {
|
|
withTmp((root) => {
|
|
writeFile(
|
|
root,
|
|
'skills/ms-ai-engineering/references/mlops-genaiops/x.md',
|
|
'# X\n\n**Status:** Reference\n\n---\n\nBody.\n',
|
|
);
|
|
const { planned, unmatched } = planCategoryBackfill(root);
|
|
assert.equal(unmatched.length, 0);
|
|
assert.equal(planned.length, 1);
|
|
assert.equal(planned[0].cat, 'MLOps & GenAIOps');
|
|
// Insert-only invariant: exactly one extra line, the Category line.
|
|
assert.match(planned[0].out, /^\*\*Category:\*\* MLOps & GenAIOps$/m);
|
|
});
|
|
});
|
|
|
|
test('planCategoryBackfill — file already carrying Category is skipped (idempotent)', () => {
|
|
withTmp((root) => {
|
|
writeFile(
|
|
root,
|
|
'skills/ms-ai-engineering/references/mlops-genaiops/x.md',
|
|
'# X\n\n**Category:** MLOps & GenAIOps\n**Status:** Reference\n\n---\n\nBody.\n',
|
|
);
|
|
const { planned, unmatched } = planCategoryBackfill(root);
|
|
assert.equal(planned.length, 0);
|
|
assert.equal(unmatched.length, 0);
|
|
});
|
|
});
|
|
|
|
test('planCategoryBackfill — file in an unknown folder is unmatched, never planned for a write', () => {
|
|
withTmp((root) => {
|
|
writeFile(root, 'skills/ms-ai-x/references/mystery/x.md', '# X\n\n**Status:** Reference\n\n---\n\nBody.\n');
|
|
const { planned, unmatched } = planCategoryBackfill(root);
|
|
assert.equal(planned.length, 0);
|
|
assert.equal(unmatched.length, 1);
|
|
assert.match(unmatched[0], /mystery/);
|
|
});
|
|
});
|