// tests/kb-update/test-taxonomy.test.mjs // Unit tests for scripts/kb-update/lib/taxonomy.mjs + data/domain-taxonomy.json. // The taxonomy is the single source of truth (lag 0) consolidating the four // previously-divergent taxonomies: poll TARGET_PREFIXES, discover INCLUDE/EXCLUDE, // category-skill-map.json, and report-changes getFilePriority. These tests pin the // behaviour the refactored scripts must reproduce, plus the no-divergence invariant. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { loadTaxonomy, saveTaxonomy, getSitemapPrefixes, getCategorySkill, makeClassifier, makeCourseClassifier, makePriorityFn, applyCategoryReassignments, } from '../../scripts/kb-update/lib/taxonomy.mjs'; const tax = loadTaxonomy(); // --- (a) sitemap prefixes — poll/discover parity --- test('sitemap_prefixes — 23 entries with poll/discover parity', () => { const prefixes = getSitemapPrefixes(tax); assert.equal(prefixes.length, 23); // poll superset includes the 6 that discover previously lacked for (const p of ['microsoftteams_en-us_', 'sharepoint_en-us_', 'microsoft-365_en-us_', 'training_en-us_', 'cloud-computing_en-us_', 'privacy_en-us_']) { assert.ok(prefixes.includes(p), `missing prefix ${p}`); } assert.ok(prefixes.includes('azure_en-us_')); // dotnet is deliberately excluded (75 sitemaps, 12 matches) assert.ok(!prefixes.includes('dotnet_en-us_')); }); // Fase 1a registry-herding: 5 stack-relevant docsets whose cited URLs were // stranded as not_in_sitemap because their child sitemaps were never polled. // Each is a single child sitemap (verified live against the sitemap index). test('sitemap_prefixes — Fase 1a additions (graph / power / ai-builder / search)', () => { const prefixes = getSitemapPrefixes(tax); for (const p of ['graph_en-us_', 'ai-builder_en-us_', 'power-apps_en-us_', 'power-automate_en-us_', 'microsoftsearch_en-us_']) { assert.ok(prefixes.includes(p), `missing Fase 1a prefix ${p}`); } }); // --- (c) category → skill — PHYSICAL DISK is canonical --- test('category_skill — disk-truth canonical (the 4 formerly stale entries)', () => { // category-skill-map.json had these as ms-ai-engineering; disk says otherwise. assert.equal(getCategorySkill(tax, 'monitoring-observability'), 'ms-ai-governance'); assert.equal(getCategorySkill(tax, 'copilot-extensibility'), 'ms-ai-advisor'); assert.equal(getCategorySkill(tax, 'prompt-engineering'), 'ms-ai-advisor'); assert.equal(getCategorySkill(tax, 'performance-scalability'), 'ms-ai-security'); // unchanged ones assert.equal(getCategorySkill(tax, 'rag-architecture'), 'ms-ai-engineering'); assert.equal(getCategorySkill(tax, 'responsible-ai'), 'ms-ai-governance'); assert.equal(getCategorySkill(tax, 'cost-optimization'), 'ms-ai-security'); // unknown category → null assert.equal(getCategorySkill(tax, 'does-not-exist'), null); }); // --- (b) classifyUrl — reproduces discover's classification exactly --- test('classifyUrl — include rules map to correct {skill, category}', () => { const classify = makeClassifier(tax); assert.deepEqual( classify('https://learn.microsoft.com/azure/ai-foundry/openai/how-to/manage-costs'), { skill: 'ms-ai-engineering', category: 'azure-ai-services' } ); // monitoring → governance (was a divergence; derivation reproduces discover's original) assert.deepEqual( classify('https://learn.microsoft.com/azure/azure-monitor/logs/foo'), { skill: 'ms-ai-governance', category: 'monitoring-observability' } ); // copilot-studio → advisor (was a divergence; derivation reproduces discover's original) assert.deepEqual( classify('https://learn.microsoft.com/microsoft-copilot-studio/fundamentals-what-is'), { skill: 'ms-ai-advisor', category: 'copilot-extensibility' } ); // alternation pattern assert.deepEqual( classify('https://learn.microsoft.com/security/benchmark/azure/foo'), { skill: 'ms-ai-security', category: 'ai-security-engineering' } ); }); test('classifyUrl — exclude wins over include and non-matches return null', () => { const classify = makeClassifier(tax); // /samples/ excluded even though /azure/ai-foundry/ would include assert.equal(classify('https://learn.microsoft.com/azure/ai-foundry/samples/quickstart'), null); // /training/ excluded assert.equal(classify('https://learn.microsoft.com/training/modules/intro'), null); // unrelated path assert.equal(classify('https://learn.microsoft.com/azure/virtual-machines/overview'), null); }); test('INVARIANT — every include category resolves and skill === category_skill[category]', () => { const classify = makeClassifier(tax); for (const rule of tax.relevance.include) { const skill = getCategorySkill(tax, rule.category); assert.ok(skill, `include category ${rule.category} absent from category_skill`); } // probe each rule via a synthetic URL and assert no skill/category divergence const probes = [ ['https://learn.microsoft.com/azure/ai-foundry/x', 'azure-ai-services'], ['https://learn.microsoft.com/azure/machine-learning/x', 'mlops-genaiops'], ['https://learn.microsoft.com/azure/search/x', 'rag-architecture'], ['https://learn.microsoft.com/azure/azure-monitor/x', 'monitoring-observability'], ['https://learn.microsoft.com/azure/well-architected/x', 'architecture'], ['https://learn.microsoft.com/microsoft-copilot-studio/x', 'copilot-extensibility'], ['https://learn.microsoft.com/purview/x', 'responsible-ai'], ['https://learn.microsoft.com/agent-framework/x', 'agent-orchestration'], ['https://learn.microsoft.com/azure/cosmos-db/x', 'data-engineering'], ]; for (const [url, expectedCat] of probes) { const r = classify(url); assert.ok(r, `no classification for ${url}`); assert.equal(r.category, expectedCat); assert.equal(r.skill, getCategorySkill(tax, r.category), `divergence at ${url}`); } }); // --- (d) getFilePriority — reproduces report-changes ordering exactly --- test('getFilePriority — pattern → priority, first match wins', () => { const prio = makePriorityFn(tax); assert.equal(prio('skills/ms-ai-security/references/cost-optimization/budget.md'), 'critical'); assert.equal(prio('skills/ms-ai-governance/references/responsible-ai/fairness.md'), 'high'); // 'governance' substring → high assert.equal(prio('skills/ms-ai-governance/references/norwegian-public-sector-governance/x.md'), 'high'); assert.equal(prio('skills/ms-ai-advisor/references/platforms/copilot-studio.md'), 'medium'); // no pattern → low assert.equal(prio('skills/ms-ai-engineering/references/data-engineering/cosmos.md'), 'low'); }); test('getFilePriority — order: cost (critical) outranks governance (high)', () => { const prio = makePriorityFn(tax); // contains both 'cost' and 'governance' — critical rule is evaluated first assert.equal(prio('skills/x/references/cost-governance-tradeoffs.md'), 'critical'); }); // --- (e) taxonomy persistence (Sesjon 19, B3 merge-apply) --- // merge-apply repoints every category owned by the absorbed skill to the // absorber and must PERSIST that to domain-taxonomy.json. Two new primitives: // a pure reassign helper + an atomic save mirroring saveDecisions. test('applyCategoryReassignments — repoints only matching from->to; pure (no mutation)', () => { const before = JSON.parse(JSON.stringify(tax.category_skill)); const next = applyCategoryReassignments(tax, [ { category: 'rag-architecture', from: 'ms-ai-engineering', to: 'ms-ai-advisor' }, // matches → repointed { category: 'responsible-ai', from: 'ms-ai-NONMATCH', to: 'ms-ai-advisor' }, // current != from → no-op { category: 'does-not-exist', from: 'whatever', to: 'ms-ai-advisor' }, // absent category → no-op ]); assert.equal(next.category_skill['rag-architecture'], 'ms-ai-advisor', 'matching entry repointed'); assert.equal(next.category_skill['responsible-ai'], 'ms-ai-governance', 'non-matching from is a no-op'); assert.equal(next.category_skill['does-not-exist'], undefined, 'absent category stays absent'); // purity: original object + its nested map are untouched, a fresh object is returned assert.deepEqual(tax.category_skill, before); assert.notEqual(next, tax); assert.notEqual(next.category_skill, tax.category_skill); }); test('saveTaxonomy + loadTaxonomy — atomic round-trip preserves content', () => { const dir = mkdtempSync(join(tmpdir(), 'tax-save-')); try { const next = applyCategoryReassignments(tax, [ { category: 'rag-architecture', from: 'ms-ai-engineering', to: 'ms-ai-advisor' }, ]); saveTaxonomy(next, dir); const reloaded = loadTaxonomy(dir); assert.equal(getCategorySkill(reloaded, 'rag-architecture'), 'ms-ai-advisor', 'reassignment persisted'); assert.equal(getCategorySkill(reloaded, 'responsible-ai'), 'ms-ai-governance', 'untouched entries survive'); assert.equal(reloaded.version, tax.version, 'non-category_skill fields survive the round-trip'); } finally { rmSync(dir, { recursive: true, force: true }); } }); // --- (f) makeCourseClassifier (C3.3) — product-slug → {skill, category} --- // course_products maps an in-domain Learn product slug → category; the owning // skill is DERIVED from category_skill (never stored redundantly), mirroring // makeClassifier's anti-divergence rule. First in-domain product wins; a course // whose products are all out-of-domain returns null (not a lead). // Synthetic taxonomy isolates the classifier's behaviour from the real data. const courseTax = { category_skill: { 'azure-ai-services': 'ms-ai-engineering', 'platforms': 'ms-ai-advisor', }, course_products: { 'azure-openai': 'azure-ai-services', 'microsoft-copilot-studio': 'platforms', 'power-automate': 'platforms', }, }; test('makeCourseClassifier — in-domain slug → derived {skill, category}', () => { const classify = makeCourseClassifier(courseTax); assert.deepEqual(classify(['azure-openai']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' }); assert.deepEqual(classify(['microsoft-copilot-studio']), { skill: 'ms-ai-advisor', category: 'platforms' }); }); test('makeCourseClassifier — first in-domain product wins (order matters)', () => { const classify = makeCourseClassifier(courseTax); // a leading out-of-domain slug is skipped; first in-domain wins assert.deepEqual(classify(['some-unknown-product', 'azure-openai']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' }); // when two in-domain products are present, the FIRST in the list wins assert.deepEqual(classify(['azure-openai', 'power-automate']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' }); assert.deepEqual(classify(['power-automate', 'azure-openai']), { skill: 'ms-ai-advisor', category: 'platforms' }); }); test('makeCourseClassifier — out-of-domain / empty / non-array → null', () => { const classify = makeCourseClassifier(courseTax); assert.equal(classify(['totally-unknown', 'another-unknown']), null, 'all out-of-domain → null'); assert.equal(classify([]), null, 'empty products → null'); assert.equal(classify(null), null, 'non-array input → null (defensive)'); assert.equal(classify(undefined), null, 'undefined input → null (defensive)'); }); test('makeCourseClassifier — taxonomy without course_products → always null', () => { const classify = makeCourseClassifier({ category_skill: {} }); assert.equal(classify(['azure-openai']), null, 'absent course_products map → no lead'); }); // --- (g) REAL course_products invariant (C3.3 data, enumerated from live API) --- test('INVARIANT — every course_products category resolves to a skill, derivation matches', () => { assert.ok(tax.course_products, 'course_products section present in domain-taxonomy.json'); assert.ok(Object.keys(tax.course_products).length > 0, 'course_products is non-empty'); const classify = makeCourseClassifier(tax); for (const [slug, category] of Object.entries(tax.course_products)) { assert.equal(typeof category, 'string', `${slug} maps to a category string`); const skill = getCategorySkill(tax, category); assert.ok(skill, `course_products slug ${slug} → category ${category} absent from category_skill`); assert.deepEqual(classify([slug]), { skill, category }, `divergence for slug ${slug}`); } }); test('makeCourseClassifier — azure-openai (verified-live slug) → engineering/azure-ai-services', () => { // azure-openai is the empirically-confirmed live slug (spike §: 34 hits). const classify = makeCourseClassifier(tax); assert.deepEqual(classify(['azure-openai']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' }); });