// @requirements REQ-PRODUCT-SERVICE-PROFILES import {test} from 'node:test';import assert from 'node:assert/strict';import {JSDOM} from 'jsdom';import {spawnSync} from 'node:child_process';import {build} from 'esbuild'; import {serviceCatalogSeed} from 'notabenda/catalog/seed';import {normalizeServiceCatalog,createCatalogServiceDraft} from 'notabenda/catalog';import {readCatalogProductProfile,renderCatalogProductProfile,formatCatalogProductProfile} from 'notabenda/catalog/profile';import {mountCatalogPicker,mountCatalogSelectionReview} from 'notabenda/catalog/ui';import {createCatalogSelectionPlan,normalizeCatalogServiceSelection} from 'notabenda/catalog/selection';import {prepareCatalogSelectionReview,confirmCatalogSelectionReview} from 'notabenda/catalog/review';import {attachCatalogReview} from 'notabenda/catalog/release';import {compileLegalDocuments} from 'notabenda/content';import {recordLegalRelease,createDocumentHistory,createAdminWorker} from 'notabenda-cf';import {createSyntheticStorage} from '../../notabenda-cf/examples/synthetic.mjs';import {fixture} from './helpers.mjs';import {mountSetup} from 'notabenda/setup/ui'; const compact=()=>{const c=structuredClone(serviceCatalogSeed);c.products=c.products.filter(p=>p.id==='stripe-payments');c.providers=c.providers.filter(p=>p.id==='stripe');const ids=new Set([...c.products[0].sourceIds,...c.providers[0].sourceIds]);c.sources=c.sources.filter(s=>ids.has(s.id));return c;}; const choose={productId:'stripe-payments',serviceId:'payments',purposeIds:['enquiries'],provider:'Actual entity',location:'Actual location',justification:'Private operator reason',confirmed:true}; const button=(root,text)=>[...root.querySelectorAll('button')].find(b=>b.textContent===text); const field=(root,text)=>[...root.querySelectorAll('label')].find(l=>l.firstChild?.textContent===text)?.querySelector('input,textarea'); test('optional profiles preserve old v1 catalogs and detached read-only notes with source status',()=>{ const c=compact();assert.equal(normalizeServiceCatalog(c).products[0].profile.schemaVersion,1);delete c.products[0].profile;assert.equal(readCatalogProductProfile(c,'stripe-payments'),undefined);assert.equal(renderCatalogProductProfile(c,'stripe-payments'),'');assert.throws(()=>readCatalogProductProfile(c,'absent')); const view=readCatalogProductProfile(serviceCatalogSeed,'resend-email');assert.ok(view.profile.facts.every(f=>f.sourceIds.every(id=>view.sources.find(s=>s.id===id)?.access==='read')));assert.equal(view.sources.find(s=>s.id==='profile-resend-dpa').checkedOn,null);view.profile.facts[0].description='Changed';assert.notEqual(readCatalogProductProfile(serviceCatalogSeed,'resend-email').profile.facts[0].description,'Changed'); assert.deepEqual(serviceCatalogSeed.products.filter(p=>p.profile).map(p=>p.id),['cloudflare-workers','resend-email','stripe-payments','supabase-platform']);assert.ok(serviceCatalogSeed.products.every(p=>p.review==='unreviewed')); }); test('profile schema rejects dangling/unread facts, unsafe URLs, duplicate IDs, coercion, controls and budgets',()=>{ for(const mutate of [ (p,c)=>p.facts[0].sourceIds=['absent'],(p,c)=>p.facts[0].sourceIds=[c.sources.find(s=>s.access==='reference-only').id],p=>p.facts[0].sourceIds=[],p=>p.facts.push(p.facts[0]),p=>p.links[0].kind=['privacy'],p=>p.links[0].url='https://stripe.com/privacy?tenant=private',p=>p.links[0].url='https://stripe.com/privacy#private',p=>p.links[0].url='https://stripe.com/other',p=>p.links.push({...p.links[0],id:'duplicate-privacy'}),p=>p.reviewPrompts[0].requirementIds=['javascript:bad'],p=>p.facts[0].description='bad\u001b[2J',p=>p.facts[0].description='x'.repeat(701),p=>p.automaticRegion='us',p=>p.schemaVersion=2, (p,c)=>{c.sources.find(s=>s.id===p.facts[0].sourceIds[0]).url+='?private=tenant';} ]){const c=compact();mutate(c.products[0].profile,c);assert.throws(()=>normalizeServiceCatalog(c));} const oversized=compact(),product=oversized.products[0],sourceId=product.profile.facts[0].sourceIds[0];product.profile.facts=Array.from({length:8},(_,i)=>({id:'fact-'+i,title:'Fact',description:'x'.repeat(700),sourceIds:[sourceId]}));product.profile.links=[]; for(let i=0;i<6;i++){const id='large-document-'+i,url='https://reference.example.test/'+('x'.repeat(1950))+i;oversized.sources.push({id,url,revision:'one',checkedOn:'2026-10-19',access:'read',license:'Original metadata',note:''});product.sourceIds.push(id);product.profile.links.push({id,kind:'documentation',label:'Document',url,sourceIds:[id]});} assert.ok(Buffer.byteLength(JSON.stringify(product.profile))>16*1024);assert.throws(()=>normalizeServiceCatalog(oversized)); const c=compact();const src=c.sources.find(s=>s.id===c.products[0].profile.facts[0].sourceIds[0]);src.id='undeclared';c.products[0].profile.facts[0].sourceIds=['undeclared'];assert.throws(()=>normalizeServiceCatalog(c)); }); test('static profiles escape all prose, visibly distinguish unread links, link prompts and exclude browser execution',async()=>{ const c=compact();c.products[0].profile.facts[0].title='';c.products[0].profile.reviewPrompts[0].prompt='';const html=renderCatalogProductProfile(c,'stripe-payments',{documentationBase:'/guide',headingLevel:4});const doc=new JSDOM(html).window.document;assert.equal(doc.querySelectorAll('script,img,input,button').length,0);assert.match(doc.querySelector('h5').textContent,/renderCatalogProductProfile(c,'stripe-payments',{documentationBase:base})); const graph=await build({stdin:{contents:"export {renderCatalogProductProfile} from 'notabenda/catalog/profile';",resolveDir:new URL('../../../',import.meta.url).pathname},bundle:true,write:false,metafile:true,platform:'browser'});assert.ok(Object.keys(graph.metafile.inputs).every(p=>!p.includes('catalog-data')&&!p.includes('catalog-project')&&!p.includes('catalog-seed')&&!p.includes('node:'))); }); test('privacy URL copying is explicit, preserves manual values and invalidates review on subsequent edits',async()=>{ const dom=new JSDOM('
'),root=dom.window.document.querySelector('#app'),config=await fixture();let calls=0; const ui=mountCatalogPicker(root,{catalog:serviceCatalogSeed,documentationBase:'/guide',getConfig:()=>config,onSelect:()=>calls++});const search=root.querySelector('input[type=search]');search.value='stripe';button(root,'Search catalog').click();button(root,'Review Stripe payment libraries').click(); const proposal=root.querySelector('.nb-catalog-proposal'),privacy=field(proposal,'Provider privacy URL (optional; verify before use)'),copy=button(proposal,'Use reference privacy URL'),confirmation=[...proposal.querySelectorAll('input[type=checkbox]')].at(-1);assert.equal(privacy.value,'');assert.ok(proposal.querySelector('.nb-catalog-profile'));assert.equal(calls,0); privacy.value='https://my-provider.test/privacy';privacy.dispatchEvent(new dom.window.Event('input',{bubbles:true}));assert.equal(copy.disabled,true);copy.click();assert.equal(privacy.value,'https://my-provider.test/privacy');privacy.value='';privacy.dispatchEvent(new dom.window.Event('input',{bubbles:true}));confirmation.checked=true;copy.click();assert.equal(privacy.value,'https://stripe.com/privacy');assert.equal(confirmation.checked,false);assert.equal(copy.disabled,true); confirmation.checked=true;field(proposal,'Actual processing location').value='new location';field(proposal,'Actual processing location').dispatchEvent(new dom.window.Event('input',{bubbles:true}));assert.equal(confirmation.checked,false);assert.equal(calls,0);assert.equal(config.services.some(s=>s.id==='stripe-payments'),false);ui.destroy(); }); test('profile-only changes are visible at re-review and retained history without rewriting legal content',async()=>{ const old=compact();delete old.products[0].profile;old.revision='old-no-profile';const initial=createCatalogSelectionPlan(await fixture(),old,choose),original=structuredClone(initial.selection);const review=prepareCatalogSelectionReview(initial.config,initial.selection,compact());assert.deepEqual(review.changes,['reference']);assert.deepEqual(initial.selection,original);assert.deepEqual(normalizeCatalogServiceSelection(original),original); const uiDom=new JSDOM('
');mountCatalogSelectionReview(uiDom.window.document.querySelector('main'),{previous:initial.selection,getConfig:()=>initial.config,catalog:compact(),onConfirm:()=>{}});button(uiDom.window.document,'Compare current reference catalog').click();await new Promise(r=>setTimeout(r,0));assert.equal(uiDom.window.document.querySelectorAll('.nb-catalog-profile').length,1); const renewed=confirmCatalogSelectionReview(initial.config,review,{confirmed:true,justification:'Reviewed new documentation references'});assert.equal(renewed.selection.service.privacyUrl,undefined);const base=compileLegalDocuments(initial.config,{mode:'preview'}),first=attachCatalogReview(base,[initial.selection]),next=attachCatalogReview(base,[renewed.selection]);assert.notEqual(first.manifest.artifactId,next.manifest.artifactId);assert.notEqual(first.manifest.catalogReview.sha256,next.manifest.catalogReview.sha256);assert.deepEqual(first.artifact.pages,next.artifact.pages);assert.doesNotMatch(Object.values(next.files).join('\n'),/From the product documentation|Regional controls have a defined scope/); const storage=createSyntheticStorage([]);await recordLegalRelease(storage,first.archive);await recordLegalRelease(storage,next.archive);const store=createDocumentHistory(storage),worker=createAdminWorker({basePath:'/admin',authorize:()=>({store,allowCatalogReview:true})});const html=await (await worker.fetch(new Request('https://example.test/admin/catalog-review-diff?before='+first.manifest.artifactId+'&after='+next.manifest.artifactId),{})).text();assert.match(html,/Documentation note roles/);assert.match(html,/From the product documentation/);assert.doesNotMatch(html,/Private operator reason/); }); test('catalog show offers readable and machine-readable references without scanning or config output',()=>{ const cli=new URL('../dist/cli.js',import.meta.url).pathname,run=(...args)=>spawnSync(process.execPath,[cli,'catalog','show',...args],{encoding:'utf8',timeout:10000});const text=run('supabase-platform');assert.equal(text.status,0,text.stderr);assert.match(text.stdout,/Hosted and self-hosted/);assert.match(text.stdout,/Documented options do not establish/);const json=run('resend-email','--json');assert.equal(json.status,0,json.stderr);assert.equal(JSON.parse(json.stdout).product.profile.schemaVersion,1); for(const args of [[],['missing'],['stripe-payments','--project','.'],['stripe-payments','--json','--json']])assert.notEqual(run(...args).status,0); }); test('profiled selection keeps setup documentation-base compatibility',async()=>{ for(const documentationBase of ['/docs/v1.0','/help/en%2DUS']){ const dom=new JSDOM('
'),root=dom.window.document.querySelector('main');const setup=mountSetup(root,{config:await fixture(),catalog:serviceCatalogSeed,documentationBase});root.querySelector('input[type=search]').value='stripe';button(root,'Search catalog').click();button(root,'Review Stripe payment libraries').click();assert.equal(root.querySelector('.nb-catalog-proposal').hidden,false);assert.ok(root.querySelector('a[href="'+documentationBase+'/requirements#req-eu-notice"]'));setup.destroy(); } });