// @requirements REQ-PRODUCT-SCOPE-GUARDS
// @requirements REQ-PRODUCT-CONTRACT-ACTIONS
// @requirements REQ-PRODUCT-SETUP
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile,mkdtemp,writeFile,rm,symlink,stat} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {spawnSync} from 'node:child_process';
import {JSDOM} from 'jsdom';
import {createSetupPlan,normalizeSetupAnswers,setupQuestions,setupMarkets,readSetupAnswers} from 'notabenda/setup';
import {mountSetup} from 'notabenda/setup/ui';
import {compileLegalDocuments} from 'notabenda/content';
import {fixture} from './helpers.mjs';
const input=()=>({schemaVersion:1,markets:[],answers:{}});
const cli=new URL('../dist/cli.js',import.meta.url).pathname;
test('unanswered practices stay unresolved, config remains draft and catalog links resolve',async()=>{
const plan=createSetupPlan(input());assert.equal(plan.report.productionReady,false);assert.ok(plan.report.recommendations.every(r=>r.status==='investigate'));assert.equal(plan.config.consent,undefined);assert.equal(plan.config.privacySignals,undefined);assert.ok(plan.report.diagnostics.some(d=>d.severity==='error'));assert.ok(Object.values(plan.config.documents).every(d=>d.review.status==='draft'));
const catalog=JSON.parse(await readFile(new URL('../../../regulatory/catalog.json',import.meta.url),'utf8'));const ids=new Set(catalog.requirements.map(r=>r.id));for(const q of setupQuestions)for(const id of q.requirements)assert.ok(ids.has(id));for(const m of setupMarkets)for(const id of m.requirements)assert.ok(ids.has(id));assert.ok(catalog.requirements.some(r=>r.id==='REQ-PRODUCT-SETUP'));
});
test('yes/no/unknown recommendations and selected markets carry reasons without deciding exemptions or activating features',()=>{
const answers=input();answers.markets=['EU-EEA','US-CA','other'];answers.answers={'optional-storage':{choice:'yes',justification:'Fictional analytics writes a marker.'},'children':{choice:'unknown',justification:''},'transfers':{choice:'no',justification:'Fictional local-only assessment.'}};
const plan=createSetupPlan(answers);assert.equal(plan.report.recommendations.find(r=>r.id==='optional-storage').status,'recommended');assert.equal(plan.report.recommendations.find(r=>r.id==='transfers').status,'declared-no');assert.equal(plan.report.recommendations.find(r=>r.id==='children').status,'investigate');assert.equal(plan.report.recommendations.find(r=>r.id==='market-US-CA').status,'review');assert.deepEqual(plan.config.jurisdictions,[]);assert.equal(plan.config.consent,undefined);assert.match(plan.report.reviewItems.join(' '),/Consent runtime not configured/);
});
test('import preserves runtime, content and operator facts, strips private extras, resets review and flags conflicting answers',async()=>{
const config=await fixture({reviewed:true});const purpose=config.services[0].purposeIds[0];config.consent={purposes:[{id:purpose,scope:'one'}],services:config.services.filter(s=>s.purposeIds.includes(purpose)).map(s=>({id:s.id,scope:'one'})),grantLifetimeDays:30,refusalLifetimeDays:180,dismissalMinutes:20};config.private={secret:'private-import-secret'};
const answers=input();answers.answers={'optional-storage':{choice:'no',justification:'Private reasoning never goes into public output.'},'personal-data':{choice:'no',justification:'Check this contradiction.'}};
const original=structuredClone(config);const plan=createSetupPlan(answers,{config});assert.deepEqual(config,original);assert.deepEqual(plan.config.consent,config.consent);assert.deepEqual(plan.config.purposes,config.purposes);assert.equal(plan.config.private,undefined);assert.deepEqual(plan.config.documents.terms.sections,config.documents.terms.sections);assert.equal(plan.config.documents.terms.review.status,'draft');assert.match(plan.report.reviewItems.join(' '),/Conflict: optional storage/);assert.match(plan.report.reviewItems.join(' '),/Conflict: personal-data/);
const build=compileLegalDocuments(plan.config,{mode:'preview'});assert.doesNotMatch(JSON.stringify(build),/Private reasoning|private-import-secret|Check this contradiction/);assert.match(JSON.stringify(plan.report),/Private reasoning/);
});
test('private necessary-storage justifications are retained separately and never rewrite public reasons',async()=>{
const config=await fixture();config.storage[0].classification='necessary';config.storage[0].necessityReason='Public synthetic necessity rationale.';const answers=input();answers.storageJustifications={[config.storage[0].id]:'Private assessment for independent review.'};const plan=createSetupPlan(answers,{config});assert.equal(plan.config.storage[0].necessityReason,config.storage[0].necessityReason);assert.match(JSON.stringify(plan.report),/Private assessment/);assert.doesNotMatch(JSON.stringify(plan.config),/Private assessment/);assert.match(plan.report.reviewItems.join(' '),/private justification recorded/);
});
test('bounded strict answer schema rejects misspelled fields, unsupported markets, duplicates and hidden activation choices',()=>{
for(const bad of [null,[],{...input(),schemaVersion:2},{...input(),markets:['EU']},{...input(),markets:['UK','UK']},{...input(),activate:true},{...input(),answers:{typo:{choice:'yes',justification:''}}},{...input(),answers:{children:{choice:true,justification:''}}},{...input(),answers:{children:{choice:'yes',justification:'x'.repeat(4001)}}},{...input(),storageJustifications:{'../storage':'bad'}},{...input(),publicFacts:{adminKey:'secret'}}])assert.throws(()=>createSetupPlan(bad));
const plan=createSetupPlan({...input(),publicFacts:{siteName:'Fictional Meadow',siteId:'fictional-meadow',siteUrl:'https://meadow.example.test'}});assert.equal(plan.config.site.id,'fictional-meadow');assert.throws(()=>createSetupPlan({...input(),publicFacts:{siteUrl:'javascript:alert(1)'}}));
const normalized=normalizeSetupAnswers(input());normalized.markets.push('UK');assert.deepEqual(input().markets,[]);
});
test('wizard CLI writes a private, ignored report alongside preview-valid draft; never overwrites or leaks notes to stdout',async()=>{
const directory=await mkdtemp(join(tmpdir(),'notabenda-setup-'));const run=(...args)=>spawnSync(process.execPath,[cli,...args],{cwd:directory,encoding:'utf8'});
try{const answers={...input(),publicFacts:{siteName:'Fictional Garden'},answers:{'optional-storage':{choice:'yes',justification:'private-wizard-note'}}};await writeFile(join(directory,'answers.json'),JSON.stringify(answers));
const result=run('wizard','--answers','answers.json');assert.equal(result.status,0,result.stderr);assert.doesNotMatch(result.stdout,/private-wizard-note/);const output=join(directory,'notabenda-setup');const config=await readFile(join(output,'notabenda.config.json'),'utf8');assert.equal(JSON.parse(config).site.name,'Fictional Garden');assert.doesNotMatch(config,/private-wizard-note/);assert.match(await readFile(join(output,'setup.private.json'),'utf8'),/private-wizard-note/);assert.equal(await readFile(join(output,'.gitignore'),'utf8'),'setup.private.json\n');assert.equal((await stat(join(output,'setup.private.json'))).mode&0o077,0);assert.equal(run('check','--config','notabenda-setup/notabenda.config.json','--preview').status,0);assert.equal(run('check','--config','notabenda-setup/notabenda.config.json').status,1);
assert.equal(run('wizard','--answers','answers.json').status,1);assert.equal(await readFile(join(output,'notabenda.config.json'),'utf8'),config);await symlink(output,join(directory,'linked'));assert.equal(run('wizard','--answers','answers.json','--out-dir','linked').status,1);assert.equal(run('wizard').status,1);assert.match(run('wizard').stderr,/needs a terminal/);
await writeFile(join(directory,'large.json'),' '.repeat(512*1024+1));assert.equal(run('wizard','--answers','large.json','--out-dir','large').status,1);await assert.rejects(stat(join(directory,'large')));
}finally{await rm(directory,{recursive:true,force:true});}
});
function browser(){const dom=new JSDOM('
',{url:'https://operator.example.test/admin/setup'});let urls=0;const revoked=[];dom.window.URL.createObjectURL=()=> 'blob:synthetic-'+(++urls);dom.window.URL.revokeObjectURL=value=>revoked.push(value);return {dom,container:dom.window.document.getElementById('setup'),revoked};}
const submit=f=>f.container.querySelector('form').dispatchEvent(new f.dom.window.Event('submit',{cancelable:true}));
/** @requirements REQ-PRODUCT-SETUP REQ-PRODUCT-A11Y */
test('setup sections and direct assessment cards preserve private answers across navigation and regenerated review',()=>{
const f=browser(),ui=mountSetup(f.container),steps=[...f.container.querySelectorAll('.nb-setup-step')],stages=[...f.container.querySelectorAll('fieldset')];
assert.equal(steps.length,6);assert.equal(stages.filter(el=>!el.hidden).length,1);assert.equal(steps[0].getAttribute('aria-current'),'step');
[...f.container.querySelectorAll('button')].find(button=>button.textContent==='Continue →').click();assert.equal(stages[1].hidden,false);assert.equal(f.dom.window.document.activeElement,stages[1].querySelector('legend'));
steps[3].click();assert.equal(stages[3].hidden,false);assert.equal(stages[0].hidden,true);assert.equal(steps[3].getAttribute('aria-current'),'step');
const question=f.container.querySelector('.nb-setup-question'),radio=question.querySelector('input[value=yes]'),reason=question.querySelector('textarea');assert.equal(question.tagName,'SECTION');radio.click();reason.value='Private supporting facts';reason.dispatchEvent(new f.dom.window.Event('input',{bubbles:true}));assert.equal(question.dataset.choice,'yes');assert.match(f.container.querySelector('.nb-setup-practice-summary').textContent,/1 yes\/no assessment/);
steps[1].click();steps[3].click();assert.equal(radio.checked,true);assert.equal(reason.value,'Private supporting facts');steps[5].click();const reviewStage=f.dom.window.document.getElementById(steps[5].getAttribute('aria-controls'));assert.equal(reviewStage.hidden,false);assert.equal(f.dom.window.document.activeElement,reviewStage);submit(f);assert.match(f.container.querySelector('.nb-setup-review-items summary').textContent,/items for your review/);assert.equal(f.container.querySelector('.nb-setup-review-items').open,false);assert.equal(f.dom.window.document.activeElement,f.container.querySelector('[aria-label="Setup recommendations"] h2'));assert.equal(ui.getPlan().report.answers.answers['personal-data'].justification,'Private supporting facts');assert.equal(f.container.querySelector('[aria-label="Setup recommendations"]').hidden,false);
steps[3].click();assert.equal(f.container.querySelector('[aria-label="Setup recommendations"]').hidden,true);reason.value='Revised privately';reason.dispatchEvent(new f.dom.window.Event('input',{bubbles:true}));assert.equal(ui.getPlan(),null);assert.equal(f.container.querySelector('a[download]'),null);submit(f);assert.equal(ui.getPlan().report.answers.answers['personal-data'].justification,'Revised privately');
ui.destroy();f.dom.window.close();
});
test('admin setup uses text-only human recommendations, detached plans and stale export invalidation',()=>{
const f=browser();const answers=input();answers.answers.children={choice:'yes',justification:'
private note'};const ui=mountSetup(f.container,{answers,documentationBase:'/guide'});submit(f);const plan=ui.getPlan();assert.ok(plan);assert.equal(f.container.querySelectorAll('a[download]').length,2);assert.ok(f.container.querySelector('a[href="/guide/requirements#req-us-coppa-scope"]'));assert.equal(f.container.querySelector('img'),null);assert.doesNotMatch(f.container.textContent,/
{
const f=browser(),ui=mountSetup(f.container);const config=await fixture();let finish;const pending=new Promise(done=>{finish=done;});const file=f.container.querySelector('input[type=file]');Object.defineProperty(file,'files',{configurable:true,value:[{size:200,text:()=>pending}]});file.dispatchEvent(new f.dom.window.Event('change'));const input=f.container.querySelector('input[type=text]');input.value='Changed during import';input.dispatchEvent(new f.dom.window.Event('input',{bubbles:true}));finish(JSON.stringify(config));await new Promise(resolve=>setImmediate(resolve));submit(f);assert.equal(ui.getPlan().config.site.name,'Changed during import');assert.deepEqual(ui.getPlan().config.services,config.services);
let stale;Object.defineProperty(file,'files',{configurable:true,value:[{size:200,text:()=>new Promise(done=>{stale=done;})}]});file.dispatchEvent(new f.dom.window.Event('change'));[...f.container.querySelectorAll('button')].find(button=>button.textContent==='Start without an imported config').click();stale(JSON.stringify(config));await new Promise(resolve=>setImmediate(resolve));submit(f);assert.deepEqual(ui.getPlan().config.services,[]);ui.destroy();f.dom.window.close();
});
test('bad import blocks export until valid import or explicit reset; external scripts cannot enter documentation links',async()=>{
const f=browser();assert.throws(()=>mountSetup(f.container,{documentationBase:'javascript:bad'}));const ui=mountSetup(f.container);const file=f.container.querySelector('input[type=file]');Object.defineProperty(file,'files',{value:[{size:5,text:async()=>'{bad'}]});file.dispatchEvent(new f.dom.window.Event('change'));await new Promise(resolve=>setImmediate(resolve));submit(f);assert.equal(ui.getPlan(),null);assert.match(f.container.textContent,/Import failed/);[...f.container.querySelectorAll('button')].find(button=>button.textContent==='Start without an imported config').click();submit(f);assert.ok(ui.getPlan());ui.destroy();f.dom.window.close();
});
test('private report resumes CLI and UI answers; storage notes survive same-config reimport and removed inventory',async()=>{
const config=await fixture();const f=browser();const ui=mountSetup(f.container,{config});const note=f.container.querySelector('fieldset:last-of-type textarea');note.value='Private note written this session.';
const files=f.container.querySelectorAll('input[type=file]');Object.defineProperty(files[0],'files',{configurable:true,value:[{size:100,text:async()=>JSON.stringify(config)}]});files[0].dispatchEvent(new f.dom.window.Event('change'));await new Promise(resolve=>setImmediate(resolve));assert.equal(f.container.querySelector('fieldset:last-of-type textarea').value,'Private note written this session.');submit(f);const saved=ui.getPlan().report;
const removed=structuredClone(config);removed.storage=[];Object.defineProperty(files[0],'files',{configurable:true,value:[{size:100,text:async()=>JSON.stringify(removed)}]});files[0].dispatchEvent(new f.dom.window.Event('change'));await new Promise(resolve=>setImmediate(resolve));submit(f);assert.equal(ui.getPlan().report.answers.storageJustifications[config.storage[0].id],'Private note written this session.');
Object.defineProperty(files[1],'files',{configurable:true,value:[{size:100,text:async()=>JSON.stringify(saved)}]});files[1].dispatchEvent(new f.dom.window.Event('change'));await new Promise(resolve=>setImmediate(resolve));submit(f);assert.equal(ui.getPlan().report.answers.storageJustifications[config.storage[0].id],'Private note written this session.');ui.destroy();f.dom.window.close();
const directory=await mkdtemp(join(tmpdir(),'notabenda-resume-'));try{await writeFile(join(directory,'report.json'),JSON.stringify(saved));const result=spawnSync(process.execPath,[cli,'wizard','--answers','report.json'],{cwd:directory,encoding:'utf8'});assert.equal(result.status,0,result.stderr);assert.match(await readFile(join(directory,'notabenda-setup/setup.private.json'),'utf8'),/Private note written this session/);}finally{await rm(directory,{recursive:true,force:true});}
});
test('setup identifies invalid public fields, preserves a keyboard path and shows actionable publication diagnostics',()=>{
const f=browser(),ui=mountSetup(f.container),steps=[...f.container.querySelectorAll('.nb-setup-step')];steps[1].click();
const url=f.container.querySelector('input[type=url]');url.value='http://example.test';const next=[...f.container.querySelectorAll('button')].find(b=>b.textContent==='Continue →');next.click();assert.equal(url.getAttribute('aria-invalid'),'true');assert.equal(f.dom.window.document.activeElement,url);assert.match(f.dom.window.document.getElementById(url.getAttribute('aria-describedby')).textContent,/HTTPS/);assert.equal(steps[1].getAttribute('aria-current'),'step');
url.value='https://example.test';url.dispatchEvent(new f.dom.window.Event('input',{bubbles:true}));url.dispatchEvent(new f.dom.window.KeyboardEvent('keydown',{key:'Enter',bubbles:true,cancelable:true}));assert.equal(steps[2].getAttribute('aria-current'),'step');assert.equal(url.hasAttribute('aria-invalid'),false);
steps[5].click();submit(f);const plan=ui.getPlan(),required=plan.report.diagnostics.filter(d=>d.severity==='error');assert.ok(required.length);assert.ok(f.container.querySelector('.nb-setup-blockers').textContent.includes(required[0].message));assert.ok(f.container.querySelector('.nb-setup-blockers').textContent.includes(required[0].path));assert.equal(f.container.querySelectorAll('a[download]').length,2);
const q=f.container.querySelector('.nb-setup-question');assert.equal(f.dom.window.document.getElementById(q.querySelector('[role=radiogroup]').getAttribute('aria-labelledby')).textContent,setupQuestions[0].title);assert.ok(q.querySelector('textarea').getAttribute('aria-label').includes('private'));ui.destroy();f.dom.window.close();
});
test('consumer practice guidance connects available forms and independent handoffs without choosing a regional profile or mutating imported facts',async()=>{
const config=await fixture({reviewed:true}),before=structuredClone(config),answers=input();answers.markets=['EU-EEA','DE','BR'];answers.answers={'consumer-payments':{choice:'yes',justification:'PRIVATE payment practice'},'consumer-distance-contracts':{choice:'yes',justification:'PRIVATE online formation'},'consumer-continuing-contracts':{choice:'yes',justification:'PRIVATE recurring service'}};
const plan=createSetupPlan(answers,{config});assert.equal(plan.report.questionnaireVersion,'3');assert.deepEqual(config,before);assert.deepEqual(plan.config.terms,config.terms);assert.deepEqual(plan.config.jurisdictions,config.jurisdictions);assert.equal(plan.config.host,undefined);assert.equal(plan.config.contractActions,undefined);assert.ok(Object.values(plan.config.documents).every(d=>d.review.status==='draft'));
const recommendation=id=>plan.report.recommendations.find(r=>r.id===id);assert.equal(recommendation('consumer-distance-contracts').status,'recommended');assert.equal(recommendation('consumer-distance-contracts').guide,'contract-actions');assert.ok(recommendation('consumer-distance-contracts').requirementIds.includes('REQ-DE-WITHDRAWAL'));assert.equal(recommendation('market-DE').status,'review');assert.match(recommendation('market-DE').reason,/not|review|Check/);assert.ok(recommendation('market-DE').requirementIds.includes('REQ-DE-CANCEL'));
assert.equal(recommendation('contract-handoffs').guide,'contract-dispatch');assert.equal(recommendation('contract-handoffs').status,'review');assert.match(recommendation('contract-handoffs').reason,/independent acknowledgement\/application/);assert.match(plan.report.reviewItems.join(' '),/No regional contract profile, route, delivery provider or scheduler has been enabled/);assert.match(JSON.stringify(plan.report),/PRIVATE online formation/);assert.doesNotMatch(JSON.stringify(compileLegalDocuments(plan.config,{mode:'preview'})),/PRIVATE payment|PRIVATE online|PRIVATE recurring|contract-handoffs/);
});
test('earlier private report revisions retain facts and leave new contract questions unresolved',()=>{
const answers=input();answers.answers['consumer-payments']={choice:'no',justification:'Private earlier assessment'};
for(const version of ['1','2','3']){const resumed=readSetupAnswers({schemaVersion:1,questionnaireVersion:version,kind:'private-setup-report',answers,recommendations:[{id:'invented',status:'recommended'}]}),plan=createSetupPlan(resumed);assert.equal(plan.report.answers.answers['consumer-payments'].choice,'no');for(const id of ['consumer-distance-contracts','consumer-continuing-contracts']){assert.equal(plan.report.answers.answers[id],undefined);assert.equal(plan.report.recommendations.find(r=>r.id===id).status,'investigate');}assert.equal(plan.report.recommendations.some(r=>r.id==='contract-handoffs'),false);assert.equal(plan.report.recommendations.some(r=>r.id==='invented'),false);}
assert.throws(()=>readSetupAnswers({schemaVersion:1,questionnaireVersion:'4',kind:'private-setup-report',answers}));
const marketOnly=createSetupPlan({...answers,markets:['DE']});assert.deepEqual(marketOnly.config.jurisdictions,[]);assert.equal(marketOnly.report.recommendations.some(r=>r.id==='contract-handoffs'),false);
});
test('contradictory paid-contract answers and a business-only terms scope require review without rewriting either',async()=>{
const config=await fixture({reviewed:true});config.terms.audience='business';const answers=input();answers.answers={'consumer-payments':{choice:'no',justification:'Private old assessment'},'consumer-continuing-contracts':{choice:'yes',justification:'Private new assessment'}};const plan=createSetupPlan(answers,{config});assert.match(plan.report.reviewItems.join(' '),/Conflict: paid continuing contracts/);assert.match(plan.report.reviewItems.join(' '),/terms.audience: the imported\/starter audience is business/);assert.equal(plan.report.answers.answers['consumer-payments'].choice,'no');assert.equal(plan.report.answers.answers['consumer-continuing-contracts'].choice,'yes');assert.equal(plan.config.terms.audience,'business');assert.equal(plan.report.productionReady,false);
});
test('CLI and admin return the same revision-3 contract guidance, private notes and real guide links',async()=>{
const answers=input();answers.markets=['DE'];answers.answers['consumer-distance-contracts']={choice:'yes',justification:'PRIVATE formation facts'};const f=browser(),ui=mountSetup(f.container,{answers,documentationBase:'/guide'});submit(f);const plan=ui.getPlan();assert.equal(plan.report.questionnaireVersion,'3');assert.ok(f.container.querySelector('a[href="/guide/contract-actions"]'));assert.ok(f.container.querySelector('a[href="/guide/contract-dispatch"]'));assert.ok(f.container.querySelector('a[href="/guide/requirements#req-de-withdrawal"]'));assert.equal(f.container.querySelectorAll('.nb-setup-question').length,setupQuestions.length);assert.ok([...f.container.querySelectorAll('[role=radiogroup]')].some(el=>f.dom.window.document.getElementById(el.getAttribute('aria-labelledby')).textContent.includes('without meeting you in person')));
const directory=await mkdtemp(join(tmpdir(),'notabenda-contract-setup-'));try{await writeFile(join(directory,'answers.json'),JSON.stringify(answers));const result=spawnSync(process.execPath,[cli,'wizard','--answers','answers.json'],{cwd:directory,encoding:'utf8'});assert.equal(result.status,0,result.stderr);const report=JSON.parse(await readFile(join(directory,'notabenda-setup/setup.private.json'),'utf8'));for(const key of ['questionnaireVersion','recommendations','reviewItems','diagnostics','productionReady'])assert.deepEqual(report[key],plan.report[key]);assert.deepEqual(report.answers.answers['consumer-distance-contracts'],plan.report.answers.answers['consumer-distance-contracts']);assert.doesNotMatch(result.stdout,/PRIVATE formation/);assert.match(result.stdout,/Config \/ integration: host.contractActions, host.contractDispatch/);assert.match(result.stdout,/Guide: https:\/\/notabenda.org\/guide\/contract-dispatch/);assert.doesNotMatch(await readFile(join(directory,'notabenda-setup/notabenda.config.json'),'utf8'),/PRIVATE formation|host.contractDispatch/);}finally{await rm(directory,{recursive:true,force:true});ui.destroy();f.dom.window.close();}
});
/** @requirements REQ-PRODUCT-SETUP REQ-PRODUCT-A11Y REQ-PRODUCT-MOBILE */
test('assessment grid exposes all answers directly, keeps independent native groups and never advances on radio Enter',()=>{
const f=browser(),second=f.dom.window.document.createElement('div');f.dom.window.document.body.append(second);const a=mountSetup(f.container),b=mountSetup(second);const steps=[...f.container.querySelectorAll('.nb-setup-step')];steps[3].click();
const cards=[...f.container.querySelectorAll('.nb-setup-practice-card')];assert.equal(cards.length,setupQuestions.length);assert.equal(f.container.querySelectorAll('.nb-setup-practice-group').length,4);assert.ok(cards.every(card=>card.tagName==='SECTION'&&card.querySelector('p').textContent&&card.querySelectorAll('input[type=radio]').length===3));
const names=[...f.dom.window.document.querySelectorAll('input[type=radio]')].map(input=>input.name);assert.equal(new Set(names).size,setupQuestions.length*2);assert.ok(cards.every(card=>card.querySelector('input[value=unknown]').checked));
const yes=cards[0].querySelector('input[value=yes]');yes.click();assert.equal(cards[0].querySelector('details').open,false,'answering needs no disclosure');assert.equal(steps[3].getAttribute('aria-current'),'step');assert.equal(second.querySelector('input[value=unknown]').checked,true);
const enter=new f.dom.window.KeyboardEvent('keydown',{key:'Enter',bubbles:true,cancelable:true});yes.dispatchEvent(enter);assert.equal(enter.defaultPrevented,true);assert.equal(steps[3].getAttribute('aria-current'),'step');
const paid=f.container.querySelector('[data-question=consumer-payments] input[value=no]');paid.click();assert.ok(f.container.querySelector('[data-question=consumer-distance-contracts]'));assert.equal(f.container.querySelectorAll('.nb-setup-practice-card').length,setupQuestions.length);
cards[0].querySelector('input[value=unknown]').click();submit(f);assert.equal(a.getPlan().report.answers.answers['personal-data'].choice,'unknown');assert.equal(a.getPlan().report.recommendations.find(r=>r.id==='personal-data').status,'investigate');assert.ok(a.getPlan().report.reviewItems.some(item=>item.includes('unresolved practice')));
a.destroy();b.destroy();f.dom.window.close();
});
test('imported assessment notes start visible and remain private through direct choices and report resume',()=>{
const answers=input();answers.answers['personal-data']={choice:'no',justification:'PRIVATE_IMPORTED_SUPPORT'};const f=browser(),ui=mountSetup(f.container,{answers});const card=f.container.querySelector('[data-question=personal-data]');assert.equal(card.querySelector('input[value=no]').checked,true);assert.equal(card.querySelector('details').open,true);assert.match(card.querySelector('summary').textContent,/Note added/);assert.equal(f.container.querySelector('[data-question=embeds] details').open,false);
card.querySelector('input[value=yes]').click();submit(f);const plan=ui.getPlan();assert.equal(plan.report.answers.answers['personal-data'].justification,'PRIVATE_IMPORTED_SUPPORT');assert.doesNotMatch(JSON.stringify(plan.config),/PRIVATE_IMPORTED_SUPPORT/);assert.match(f.container.querySelector('.nb-setup-practice-summary').textContent,/1 supporting note/);card.querySelector('textarea').value='';card.querySelector('textarea').dispatchEvent(new f.dom.window.Event('input',{bubbles:true}));assert.equal(ui.getPlan(),null);assert.doesNotMatch(card.querySelector('summary').textContent,/Note added/);ui.destroy();f.dom.window.close();
});
/** @requirements REQ-PRODUCT-SETUP REQ-PRODUCT-SERVICE-DETECTION REQ-PRODUCT-DISCOVERY */
test('browser setup advises offline project discovery without running it or treating findings as configured services',()=>{
const f=browser(),ui=mountSetup(f.container,{documentationBase:'/guide'}),guide=f.container.querySelector('.nb-setup-discovery');assert.ok(guide);assert.equal(guide.querySelector('code').textContent,'npx notabenda discover --project . --out-dir ./service-review');assert.equal(guide.querySelector('a').getAttribute('href'),'/guide/service-catalog#search-a-project-with-npx');assert.ok(guide.querySelector('.nb-setup-command-flag'));assert.equal(guide.querySelector('pre').tabIndex,0);assert.equal(guide.querySelectorAll('button,form,script,iframe').length,0);assert.match(guide.textContent,/before selecting services/);assert.match(guide.textContent,/server dependencies or infrastructure/);assert.match(guide.textContent,/some services may be missed/);submit(f);const review=f.container.querySelector('[aria-label="Setup recommendations"] .nb-setup-discovery');assert.ok(review);assert.equal(review.querySelector('code').textContent,guide.querySelector('code').textContent);assert.equal(review.closest('[hidden]'),null);assert.equal(ui.getPlan().config.services.length,0);assert.equal(ui.getPlan().report.catalogSelections,undefined);submit(f);assert.equal(f.container.querySelectorAll('[aria-label="Setup recommendations"] .nb-setup-discovery').length,1);ui.destroy();f.dom.window.close();
});
/** @requirements REQ-NZ-INDIRECT REQ-PRODUCT-INDIRECT-NOTICE */
test('imported indirect inventory receives notification guidance without enabling a profile or rewriting facts',async()=>{
const config=await fixture({reviewed:true});config.purposes[0].collection.source='both';const before=structuredClone(config),plan=createSetupPlan({schemaVersion:1,markets:[],answers:{}},{config});const recommendation=plan.report.recommendations.find(r=>r.id==='indirect-collection');assert.equal(recommendation.status,'review');assert.equal(recommendation.guide,'indirect-collection');assert.ok(recommendation.requirementIds.includes('REQ-NZ-INDIRECT'));assert.match(recommendation.reason,/select it only after assessing applicability/);assert.deepEqual(plan.config.purposes,config.purposes);assert.deepEqual(config,before);assert.equal(Object.hasOwn(plan.config,'indirectCollectionNotices'),false);
});