22 lines
5.2 KiB
JavaScript
22 lines
5.2 KiB
JavaScript
import fs from 'node:fs';import path from 'node:path';import vm from 'node:vm';import crypto from 'node:crypto';import assert from 'node:assert/strict';
|
|
const read=p=>JSON.parse(fs.readFileSync(p,'utf8')),state=read('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),tag=process.argv[2];if(state.port!==55439||!/^[a-f0-9]{12}$/.test(tag??''))throw Error('Owned rendered fixture required');const root=path.join(state.root,'private/ledger-'+tag),output=path.join(root,'sales-forms.json');if(fs.existsSync(output))throw Error('Completed form proof preserved');
|
|
const html=fs.readFileSync(path.join(root,'sales-demo-preparer.html'),'utf8'),script=[...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].at(-1)?.[1];if(!script)throw Error('Rendered script missing');let checks=0;const check=(v,s)=>{checks++;assert(v,s);};
|
|
const finance={dataset:{finance:'propose'},fields:{kind:'receipt',obligation_id:'9',revision:'1',amount:'400.00',date:'2026-10-29',reason:'Individual receipt reason.',evidence:'Synthetic only.'},button:{},querySelector(){return this.button;}},ledger={dataset:{ledger:'validate'},fields:{proposal_id:'8',revision:'a'.repeat(64),decision:'approved',reason:'Independent exact snapshot review.'},button:{},querySelector(){return this.button;}};
|
|
const error={textContent:''},pdf={},link={dataset:{},hidden:true},requests=[],mock={token:null,module:'accounting',counter:0,failCommand:false,failPdf:false};let page;
|
|
const sandbox={FormData:class{constructor(form){return new Map(Object.entries(form.fields));}},crypto:{randomUUID:crypto.randomUUID},document:{querySelectorAll:()=>[finance,ledger],querySelector:s=>s==='#pdf'?pdf:s==='#pdf-link'?link:error},location:{reload(){},assign(){}},URL:{createObjectURL:()=> 'blob:qa-rendered-pdf',revokeObjectURL(){}},fetch:async(url,options={})=>{const body=options.body?JSON.parse(options.body):null;requests.push({url,options,body});
|
|
if(url==='/api/session')return{ok:true,json:async()=>({user:{id:page.actor},context:{token:mock.token,company:{id:page.scope.company_id,tenant_id:page.scope.tenant_id},unit:{id:page.scope.unit_id},module_id:mock.module}})};
|
|
if(url==='/api/context'){mock.module=body.module_id;mock.token='mock-generation-'+(++mock.counter);return{ok:true,json:async()=>({context:{token:mock.token}})};}
|
|
if(url.endsWith('/pdf'))return{ok:!mock.failPdf,blob:async()=>({type:'application/pdf'})};
|
|
return{ok:!mock.failCommand,json:async()=>mock.failCommand?{message:'Expected stale source refusal.'}:{id:1}};
|
|
}};
|
|
vm.createContext(sandbox);vm.runInContext(script,sandbox);page=vm.runInContext('({scope,invoice,actor,context})',sandbox);mock.token=page.context;
|
|
const submit=form=>form.onsubmit({preventDefault(){}}),posts=()=>requests.filter(r=>r.url==='/api/service-receivables/proposals');
|
|
await submit(finance);let first=posts().at(-1);check(first.body.invoice_id===page.invoice&&first.body.obligation_id===9&&first.body.revision===1&&first.body.amount==='400.00','Financial IDs/revision typed; amount stays exact string');check(first.body.external_id===first.options.headers['Idempotency-Key'],'External factual identity and request receipt share stable key');check(mock.module==='accounting'&&first.options.headers['X-SGF-Context']==='mock-generation-1','Financial write uses fresh Finance generation and returns Accounting');
|
|
await submit(finance);check(posts().at(-1).options.headers['Idempotency-Key']===first.options.headers['Idempotency-Key']&&JSON.stringify(posts().at(-1).body)===JSON.stringify(first.body),'Unchanged retry retains same request/factual identity');
|
|
finance.fields.reason='Corrected individual receipt reason.';await submit(finance);check(posts().at(-1).options.headers['Idempotency-Key']!==first.options.headers['Idempotency-Key'],'Changed content gets new key');
|
|
await submit(ledger);const reviewed=requests.filter(r=>r.url==='/api/general-ledger/validate').at(-1);check(reviewed.body.proposal_id===8&&reviewed.body.revision==='a'.repeat(64),'Ledger revision remains exact hash, not numeric conversion');
|
|
mock.failCommand=true;await submit(finance);check(mock.module==='accounting'&&error.textContent.includes('Expected stale source refusal.'),'Financial refusal restores current Accounting context and reports error');mock.failCommand=false;
|
|
const before=posts().length;mock.token='another-tab-context';mock.module='commercial';await submit(finance);check(posts().length===before&&mock.module==='commercial','Stale form does not refresh itself into a financial decision or overwrite another context');
|
|
mock.token=vm.runInContext('context',sandbox);mock.module='accounting';mock.failPdf=true;await pdf.onclick();check(mock.module==='accounting','Failed PDF fetch returns to current Accounting context');mock.failPdf=false;await pdf.onclick();check(mock.module==='accounting'&&!link.hidden&&link.href==='blob:qa-rendered-pdf','PDF retrieval produces explicit link and restores context');
|
|
fs.writeFileSync(output,JSON.stringify({passed:true,checks,rendered_script_sha256:crypto.createHash('sha256').update(script).digest('hex'),mock_dom:true,actual_browser:false,no_business_requests:true,exact_amount_and_hash_types:true,stable_repetition:true,stale_generation_cannot_self_refresh:true,pdf_failure_recovery:true},null,2)+'\n');console.log(JSON.stringify({passed:true,checks,mock_dom:true,actual_browser:false}));
|