// @requirements REQ-PRODUCT-SERVICE-CATALOG REQ-PRODUCT-ARTIFACTS REQ-PRODUCT-CF-HISTORY REQ-PRODUCT-RETAINED-RELEASES REQ-PRODUCT-AUTHORING import {test} from 'node:test';import assert from 'node:assert/strict'; import {readFile,mkdtemp,writeFile,rm,stat,symlink} from 'node:fs/promises';import {tmpdir} from 'node:os';import {join} from 'node:path';import {spawnSync} from 'node:child_process'; import {attachCatalogReview} from 'notabenda/catalog/release';import {serviceCatalogSeed} from 'notabenda/catalog/seed';import {createCatalogSelectionPlan} from 'notabenda/catalog/selection';import {createSetupPlan} from 'notabenda/setup'; import {compileLegalDocuments,createRetainedReleaseFiles,canonicalJson,sha256,createTermsOffer} from 'notabenda/content';import {compileAuthoredDocuments} from 'notabenda-authoring'; import {recordLegalRelease,createDocumentHistory,validateLegalSourceArchive,createAdminWorker} from 'notabenda-cf';import {validateLegalArtifact,validateStoredLegalRelease} from '../../notabenda-cf/dist/history.js'; import {createSyntheticStorage} from '../../notabenda-cf/examples/synthetic.mjs';import {fixture} from './helpers.mjs'; const selected=async()=>createCatalogSelectionPlan(await fixture({reviewed:true}),serviceCatalogSeed,{productId:'stripe-payments',serviceId:'payments',purposeIds:['enquiries'],provider:'Explicit entity',location:'Selected region',justification:'PRIVATE_JUSTIFICATION_SENTINEL',confirmed:true}); const query={query:'',kind:'',keyPrefix:'',from:'',to:'',limit:25}; const request=path=>new Request('https://example.test/admin'+path); async function pair(){const {config,selection}=await selected();selection.reference.sources[0].note+=' RETAINED_REFERENCE_ONLY_SENTINEL';const base=compileLegalDocuments(config,{mode:'preview'}),first=attachCatalogReview(base,[selection]);const later=structuredClone(selection);later.reference.revision+='-new';return {config,selection,base,first,latest:attachCatalogReview(base,[later])};} test('finalization binds selected facts without changing legal bytes or publishing private notes, seeds or questionnaires',async()=>{ const {config,selection,base,first,latest}=await pair(),original=structuredClone(base);assert.notEqual(first.manifest.artifactId,base.manifest.artifactId);assert.notEqual(first.manifest.artifactId,latest.manifest.artifactId);assert.deepEqual(base,original); assert.deepEqual(first.artifact.pages,base.artifact.pages);assert.deepEqual(first.manifest.documents,base.manifest.documents);assert.deepEqual(first.publicConfig,base.publicConfig);assert.equal(first.manifest.catalogReview.sha256,sha256(canonicalJson(first.archive.catalogReview)));assert.equal(first.manifest.catalogReview.selectionCount,1); assert.equal(first.archive.catalogReview.selections[0].reference.products.length,1);assert.deepEqual(first.archive.catalogReview.selections[0].reference.products[0].identifiers,[]); assert.doesNotMatch(JSON.stringify(first),/PRIVATE_JUSTIFICATION_SENTINEL/);for(const value of Object.values(first.files))assert.doesNotMatch(value,/catalog-release-review|operator-confirmed|catalogSelections|private-setup-report|justification/); assert.equal(Object.hasOwn(first.files,`artifacts/${base.manifest.artifactId}.json`),false);assert.equal(createTermsOffer(first.artifact,{scopeId:'join',statement:'I agree',actionLabel:'Agree'},{mode:'preview'}).artifactId,first.manifest.artifactId); const noteOnly=structuredClone(selection);noteOnly.justification='Another private reason';assert.equal(attachCatalogReview(base,[noteOnly]).manifest.artifactId,first.manifest.artifactId);assert.throws(()=>attachCatalogReview(first,[selection]),/fresh/); await validateLegalArtifact(first.artifact);await validateLegalSourceArchive(first.archive);const retained=createRetainedReleaseFiles([base.artifact,first.artifact,latest.artifact],{siteId:config.site.id,mode:'preview'});assert.equal(retained[`artifacts/${first.manifest.artifactId}/terms.html`],base.artifact.pages['terms/index.html']); for(const name of ['catalog-profile.ts','catalog-reference.ts','catalog-plain.ts','catalog-release-policy.ts'])assert.equal(await readFile(new URL('../src/'+name,import.meta.url),'utf8'),await readFile(new URL('../../notabenda-cf/src/'+name,import.meta.url),'utf8')); }); test('stale or unrelated pins, duplicate services and private attachment changes fail before archival',async()=>{ const {config,selection,base,first}=await pair(); for(const change of [c=>c.services.at(-1).name='Changed',c=>c.services.at(-1).provider='Changed',c=>c.services.pop(),c=>c.purposes[0].retention='Changed']){const c=structuredClone(config);change(c);assert.throws(()=>attachCatalogReview(compileLegalDocuments(c,{mode:'preview'}),[selection]));} assert.throws(()=>attachCatalogReview(base,[selection,selection]));assert.throws(()=>attachCatalogReview(base,[])); await assert.rejects(validateLegalSourceArchive(first.artifact),/catalog review/); for(const mutate of [a=>delete a.catalogReview,a=>a.catalogReview.selections[0].reference.revision+='tampered',a=>a.catalogReview.selections[0].justification='PRIVATE_SENTINEL',a=>a.catalogReview.selections[0].service.provider='Changed',a=>a.catalogReview.selections[0].purposes[0].retention='Changed']){const a=structuredClone(first.archive);mutate(a);await assert.rejects(validateLegalSourceArchive(a));} await assert.rejects(validateLegalSourceArchive({schemaVersion:1,artifact:base.artifact,sourceAttachments:{},catalogReview:first.archive.catalogReview})); const storage=createSyntheticStorage([]);assert.deepEqual(await recordLegalRelease(storage,first.archive),{id:first.manifest.artifactId,created:true});assert.deepEqual(await recordLegalRelease(storage,first.archive),{id:first.manifest.artifactId,created:false});await validateStoredLegalRelease(await storage.get('notabenda/releases/'+first.manifest.artifactId)); await assert.rejects(recordLegalRelease(storage,first.archive,{changeNote:'New metadata'}),/immutable/);await recordLegalRelease(storage,base.artifact);assert.equal(await createDocumentHistory(storage).getCatalogReview(base.manifest.artifactId),undefined); }); test('authored sources, output and private reviews share one validated immutable archive',async()=>{ const {config,selection}=await selected();const authored=await compileAuthoredDocuments(config,{terms:{format:'md',source:'---\nkind: terms\ntitle: Authored terms\nroute: /terms\nversion: authored-1\neffectiveDate: 2026-10-19\nsource: supplied\nreview:\n status: draft\n---\n## Provision\nA retained authored provision.\n'}},{mode:'preview'}); const result=attachCatalogReview(authored,[selection]);assert.deepEqual(result.archive.sourceAttachments,authored.archive.sourceAttachments);assert.deepEqual(result.artifact.renderedBodies,authored.artifact.renderedBodies);assert.deepEqual(result.fragments,authored.fragments);assert.deepEqual(result.artifact.pages,authored.artifact.pages);await validateLegalSourceArchive(result.archive);const broken=structuredClone(authored);broken.archive.sourceAttachments.terms.source+='changed';assert.throws(()=>attachCatalogReview(broken,[selection]),/sources no longer match/); const storage=createSyntheticStorage([]);await recordLegalRelease(storage,result.archive);const store=createDocumentHistory(storage);assert.match((await store.getSource(result.manifest.artifactId,'terms')).data.source,/retained authored/);assert.ok(await store.getCatalogReview(result.manifest.artifactId)); }); test('catalog history permission is independent and ordinary list, detail and export omit attachment facts',async()=>{ const {first,latest}=await pair(),storage=createSyntheticStorage([]);for(const build of [first,latest])await recordLegalRelease(storage,build.archive);const store=createDocumentHistory(storage);let privateReads=0;const guarded={...store,getCatalogReview:async id=>{privateReads++;return store.getCatalogReview(id);}}; for(const allowed of [undefined,false,true]){ const worker=createAdminWorker({basePath:'/admin',authorize:()=>({store:guarded,allowExport:true,...(allowed===undefined?{}:{allowCatalogReview:allowed})})}); const view=await worker.fetch(request('/catalog-review?id='+first.manifest.artifactId),{});assert.equal(view.status,allowed?200:403); if(allowed){const html=await view.text();assert.match(html,/Explicit entity/);assert.match(html,/Selected region/);assert.doesNotMatch(html,/PRIVATE_JUSTIFICATION_SENTINEL/);assert.match(view.headers.get('Cache-Control'),/private/);assert.doesNotMatch(html,/