import {test} from 'node:test'; import assert from 'node:assert/strict'; import {readFileSync, mkdtempSync, rmSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {execFileSync} from 'node:child_process'; import {skillsViewer,isSkillsStory} from './skills-viewer.mjs'; const read = p => readFileSync(p,'utf8'); const topics = ['governance','quality','adoption','measurement','maintenance']; test('all six Skills Adoption pages account for exactly five diagrams and ten sources',()=>{ let count=0; for(const route of ['stage-01-skills-adoption',...topics.map(t=>t==='measurement'?t:`skills-${t}`)]){ const diagrams=[...read(`docs/strategy/${route === 'measurement' ? 'measurement/index' : route}.mdx`).matchAll(/{ const name=`strategy-${topic}`;const desktop=JSON.parse(read(`diagrams/${name}.architecture.json`)); const mobile=JSON.parse(read(`diagrams/${name}-mobile.architecture.json`)); const semantic = d=>d.components.map(({pos,size,...n})=>n); assert.deepEqual(semantic(desktop),semantic(mobile)); assert(new Set(desktop.components.map(n=>n.pos[0])).size>1); assert.equal(new Set(mobile.components.map(n=>n.pos[0])).size,1); for(const n of desktop.components){assert(n.tag);assert(n.sublabel);assert(n.sublabel.split('\n').length<=4);} const dir=mkdtempSync(join(tmpdir(),'skills-viewer-')); try{ const path=join(dir,'native.html'); execFileSync(process.execPath,['vendor/archify/renderers/architecture/render-architecture.mjs',`diagrams/${name}.architecture.json`,path]); const native=read(path);const html=skillsViewer(native,desktop); assert.equal((html.match(/]*data-node-id=/g)||[]).length,desktop.components.length); assert.equal((html.match(/skillsViewer(native.replace('data-node-id="'+desktop.components[0].id+'"','data-node-id="missing"'),desktop)); const bad=structuredClone(desktop);bad.components[0].sublabel='';assert.throws(()=>skillsViewer(native,bad)); }finally{rmSync(dir,{recursive:true,force:true});} }); test('domain contracts retain human authority, honest measures and Delivery-first rollout',()=>{ const text=t=>JSON.stringify(JSON.parse(read(`diagrams/strategy-${t}.architecture.json`))); assert(text('governance').includes('Author is not sole approver')); assert(text('quality').includes('Critical effect blocks')); assert(text('adoption').includes('Delivery first')&&text('adoption').includes('Later order undecided')); assert(text('adoption').includes('No quotas or rankings')); for(const marker of ['Tracker + acceptance','Invoices + review minutes','Changes + defects','Anonymous 1–5 survey','Missing buckets = partial','Zero denominator = N/A'])assert(text('measurement').includes(marker),marker); assert(text('maintenance').includes('No automatic reinstatement')); }); test('topic compositions explain distinct relationships rather than a fixed card count',()=>{ const spec=t=>JSON.parse(read(`diagrams/strategy-${t}.architecture.json`)); const has=(d,a,b)=>assert(d.connections.some(e=>e.from===a&&e.to===b),`${a} → ${b}`); const g=spec('governance');const steps=g.components.filter(n=>/^0[1-6]/.test(n.label)); assert.equal(steps.length,6); for(let i=1;isteps[i-1].pos[0]);assert(steps[i].pos[1]>steps[i-1].pos[1]);has(g,steps[i-1].id,steps[i].id);} has(g,'approve','owner');has(g,'distribute','withdraw'); const q=spec('quality');for(const id of ['selection','safety','artifact','outcome'])has(q,id,'gate');has(q,'gate','accept');has(q,'gate','rework'); const a=spec('adoption');for(const [x,y] of [['human','agent'],['agent','human'],['agent','learn'],['learn','delivery'],['learn','expand'],['human','expand']])has(a,x,y); const m=spec('measurement');for(const id of ['success','quality','cost','confidence']){has(m,id+'-source',id);has(m,id,'interpret');} const c=spec('maintenance');for(const [x,y] of [['observe','triage'],['triage','change'],['change','validate'],['validate','release'],['release','observe'],['triage','safe'],['safe','observe'],['triage','retire'],['triage','contain'],['contain','change']])has(c,x,y); });