// Offline cross-level conformance, not visual browser acceptance.
const fs=require('fs'),path=require('path'),vm=require('vm'),assert=require('assert');
const repo=path.resolve(__dirname,'../../../..');
const site=path.join(repo,'docs'),wiki=path.join(repo,'src','docs'),DOCS='technical-documentation';
const full=fs.readFileSync(path.join(__dirname,'case_journeys.html'),'utf8');
const preview=fs.readFileSync(path.join(__dirname,'operations_preview.html'),'utf8');
const scripts=s=>[...s.matchAll(/'))[1];
for(const m of html.matchAll(/data-view="([^"]+)"/g)){const n=node('view-'+m[1]);n.dataset.view=m[1];views.push(n);}
const context=vm.createContext({document:{getElementById:node,querySelector:()=>({open:false}),querySelectorAll:()=>views},location:{hash,search},history:{replaceState(){}},window:{addEventListener(){}},URLSearchParams});
for(const s of scripts(html))vm.runInContext(s,context);
return {context,node,run:code=>vm.runInContext(code,context)};
}
const a=harness(full),b=harness(preview);
const configs=b.run('previewViews');
assert.equal(Object.keys(configs).length,6);
for(const [key,value] of Object.entries(configs)){
b.run('choosePreviewView('+JSON.stringify(key)+')');
a.run("selected=cases.find(c=>c.id==="+JSON.stringify(value[1])+");resetStage();activeUid="+JSON.stringify(value[2])+";surface="+JSON.stringify(value[3])+";stateUid="+JSON.stringify(value[4])+";scenario='first';chatThreads[threadKey(active())]="+JSON.stringify(value[5])+";render();");
assert.equal(b.node('preview').innerHTML,a.node('preview').innerHTML,key+' frame is identical');
const url=new URL(b.node('preview-open').href,'https://fixture.invalid/');
const reopened=harness(full,url.search,url.hash);
assert.equal(reopened.node('preview').innerHTML,b.node('preview').innerHTML,key+' deep link preserves the operation');
if(key==='decision'){
b.run("act('approve')");a.run("act('approve')");
assert.equal(b.node('preview').innerHTML,a.node('preview').innerHTML,'Decision action is identical at both levels');
}
}
const index=fs.readFileSync(path.join(site,'index.html'),'utf8');
const ui=fs.readFileSync(path.join(wiki,'10 Architecture/UI.md'),'utf8');
assert(index.includes('src="'+DOCS+'/10-architecture/ui/operations_preview.html?view=decision'),'the goals example embeds the configuration decision screen');
assert(ui.includes('src="ui/operations_preview.html"'));
// the repeated hero panels are gone: one case is worked in the goals section instead, with its own
// workflow strip and deep links to its remaining screens.
assert(!index.includes('data-operations-accordion')&&!index.includes('data-accordion-trigger')&&!index.includes('case-intake.svg'));
assert(index.includes('id="one-case"')&&index.includes('class="ge-tech"')&&index.includes('class="ge-zoom"'));
const journeys=a.run('traces');
const knownLabels=new Map();
for(const t of Object.values(journeys))for(const s of [...t.journey.root,t.journey.closure,...t.journey.paths.first,...t.journey.auxiliary,...t.journey.linked]){
const key=s.module+'/'+s.stage;if(!knownLabels.has(key))knownLabels.set(key,new Set());knownLabels.get(key).add(s.label);
}
// The shared module flow strip was removed from the homepage in c1a371ef, so there are no
// data-module-stage cells left to agree with; the loop stays because it guards the vocabulary
// again the moment any of them come back. Its companion caption assertion went with the strip.
for(const match of index.matchAll(/data-module-stage="([^"]+)"[^>]*>([^<]+)<\/b>/g))assert(knownLabels.get(match[1])?.has(match[2].replaceAll('&','&')),match[1]+' public vocabulary agrees');
assert(!index.includes('The same seven steps'));
b.run("choosePreviewView('decision')");
assert(b.node('preview').innerHTML.includes('Mira D.'));
assert(b.node('preview').innerHTML.includes('Ablera operator'));
assert(b.node('preview').innerHTML.includes('Parent → child · PlanConfirmation'));
assert(b.node('preview').innerHTML.includes('KOLHIDA_EUR · sub-plan'));
assert(b.node('preview').innerHTML.includes('chat-composer'));
const before=b.run('JSON.stringify(feedback)');
b.node('chat-text').value='Also check the rejected role ';
b.run("act('send-chat')");assert(b.node('preview').innerHTML.includes('<script>'));
assert.equal(b.run('JSON.stringify(feedback)'),before,'A chat message does not approve a gate');
b.run("act('approve')");assert.equal(b.run('JSON.stringify(feedback)'),before,'Unreconciled steering prevents approval of the old packet');
b.run("act('thread:template-1')");assert(b.node('preview').innerHTML.includes('MAXO_EUR · sub-plan'));
assert(b.node('preview-open').href.includes('thread=template-1'));
b.run("choosePreviewView('approval')");assert(b.node('preview').innerHTML.includes('Georgi S.'));assert(!b.node('preview').innerHTML.includes('id="chat-composer"'));
b.run("choosePreviewView('result')");assert(!b.node('preview').innerHTML.includes('Abacus template task'));assert(b.node('preview').innerHTML.includes('Conversation with Ablera'));
// the homepage no longer restates the CF-01 exception (removed with the flow strip in c1a371ef);
// the case viewer is still the place it has to be right.
const cf1=harness(full,'','#CF-01');assert(cf1.node('route-exception').innerHTML.includes('catalogue prefix'));
const cfCatalog=fs.readFileSync(path.join(wiki,'10 Architecture/20 Configuration/Typical Cases.md'),'utf8');
const cf1paragraph=cfCatalog.match(/\*\*CF-01[^\n]+/)[0];assert(cf1paragraph.includes('Current illustrated path [P]'));assert(!cf1paragraph.includes('no shared rows'));assert(!cf1paragraph.includes('S3 apply on PROD'));
console.log('Six chat/plan examples: shared frames/actions, operators, composer, nested parent confirmation, exact links, the goals-section case embed and CF-01 routing consistency pass. DOM stub only.');