import fs from 'node:fs';import path from 'node:path';import net from 'node:net';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {spawn} from 'node:child_process'; 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 cost fixture required');const root=path.join(state.root,'private/ledger-'+tag),g=read(path.join(root,'cost-fixture.json')),out=path.join(root,'industrial-http.json');if(fs.existsSync(out)||!fs.existsSync(path.join(root,'industrial-fixture.json')))throw Error('Fresh HTTP proof with private baseline required'); await new Promise((resolve,reject)=>{const s=net.createServer();s.once('error',reject);s.listen(8047,'127.0.0.1',()=>s.close(resolve));});const log=fs.openSync(path.join(root,'cost-http-server.log'),'a'),server=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['-S','127.0.0.1:8047',path.resolve('scripts/ledger-cost-http-router.php')],{windowsHide:true,stdio:['ignore',log,log],env:{...process.env,LEDGER_QA_SETTINGS:path.join(root,'settings.json')}}); let checks=0;const ok=(v,m)=>{checks++;assert(v,m);};const client=()=>{const jar=new Map();let auth;const request=async(url,body,key=crypto.randomUUID(),extra={})=>{const r=await fetch('http://127.0.0.1:8047'+url,{method:body?'POST':'GET',headers:{Accept:'application/json',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),'X-SGF-Context':auth?.context??'',...(body?{'Content-Type':'application/json','X-CSRF-TOKEN':auth?.csrf??'','Idempotency-Key':key}:{}),...extra},body:body?JSON.stringify(body):undefined,redirect:'manual',signal:AbortSignal.timeout(15000)});for(const c of r.headers.getSetCookie()){const p=c.split(';')[0],i=p.indexOf('=');jar.set(p.slice(0,i),p.slice(i+1));}const text=await r.text();let data;try{data=JSON.parse(text);}catch{}return{status:r.status,data,text,headers:r.headers};};return{request,enter:async role=>{const r=await request('/qa/cost-enter/'+role);ok(r.status===200,'Owned '+role+' entry');auth=r.data;ok(jar.has('sgf_cost_'+tag)&&!jar.has('XSRF-TOKEN'),'Separate session cookie and shared CSRF preserved');},module:async module=>{const old=auth.context,r=await request('/api/context',{...g.scope,module_id:module});ok(r.status===200,'Explicit module transition');auth={csrf:r.data.csrf,context:r.data.context.token};return old;}};}; const fixture=read(path.join(root,'industrial-fixture.json')),api='/api/general-ledger/industrial?lot_id='+fixture.lot_id,html='/general-ledger/industrial?lot_id='+fixture.lot_id; const command=async(c,a,b,key)=>{const r=await c.request('/api/general-ledger/'+a,b,key);ok(r.status===200,a+' '+r.status+' '+(r.data?.message??''));return r.data;}; try{ const p=client(),v=client(),r=client(),tech=client();let ready=false;for(let n=0;n<80&&!ready;n++){try{ready=(await r.request(api)).status===401;}catch{}if(!ready)await new Promise(res=>setTimeout(res,100));}ok(ready,'Unauthenticated industrial read denied');await p.enter('prepare');await v.enter('validate');await r.enter('read');await tech.enter('technician');ok((await tech.request(api)).status===403,'Technical role does not inherit industrial accounting'); let d=await r.request(api,undefined,undefined,{'X-Cost-QA-Proof':'read'});ok(d.status===200&&d.data.industrial.source.principal_minor===180000&&d.data.industrial.source.residual_exact==='300'&&d.data.book.journals.length===0,'Actual1800 with residual300 not yet posted');const mode=read(path.join(root,'cost-http-read.json'));ok(mode.transaction.isolation==='repeatable read'&&mode.transaction.read_only==='on'&&mode.writer_locks===0,'Industrial conference and book one coherent read without writer lock');let view=await p.request(html);ok(view.status===200&&view.text.includes('data-action="effect-prepare"')&&view.text.includes('QA-CUSTO-INDUSTRIAL')&&view.text.includes('Residual 300 MZN'),'Explicit industrial impact before confirmation');fs.writeFileSync(path.join(root,'industrial-preparer.html'),view.text);view=await r.request(html);ok(view.status===200&&!view.text.includes('data-action='),'Reader sees evidence without decisions');d=await r.request('/api/general-ledger/workspace');ok(d.status===200&&d.data.panels.industrial.rows.length===1&&d.data.panels.industrial.rows[0].original_journal_id===null,'Bounded industrial panel in combined workspace'); for(const query of ['lot_id=0','lot_id=999999','lot_id%5Bx%5D=1','lot_id='+fixture.lot_id+'&company_id=9'])ok([403,422].includes((await r.request('/api/general-ledger/industrial?'+query)).status),'Invalid/foreign lot rejected'); const body={effect_type:'industry.cost.validated',source_id:fixture.lot_id,journal_on:'2026-10-29',period_key:'QA-2026-10',rule_key:'QA-INDUSTRIAL',reason:'Actual independently validated industrial1800, residual300 separate.'};ok((await p.request('/api/general-ledger/effect-prepare',body,undefined,{'X-CSRF-TOKEN':''})).status===419,'Real CSRF validation');ok((await r.request('/api/general-ledger/effect-prepare',body)).status===403,'Reader cannot prepare');const old=await p.module('finance');ok((await p.request(api)).status===409,'Accounting module required');await p.module('accounting');ok((await p.request('/api/general-ledger/effect-prepare',body,undefined,{'X-SGF-Context':old})).status===409,'Stale context generation rejected'); const key=crypto.randomUUID(),proposal=await command(p,'effect-prepare',body,key);ok((await command(p,'effect-prepare',body,key)).replayed,'Capture receipt replay');view=await v.request(html);ok(view.status===200&&view.text.includes('data-action="validate"')&&view.text.includes('1800'),'Reviewer sees exact captured industrial evidence');const review={proposal_id:proposal.proposal_id,revision:proposal.revision,decision:'approved',reason:'Independent exact industrial result and two-line impact review.'};ok((await p.request('/api/general-ledger/validate',review)).status===403,'Preparer cannot independently review');ok((await v.request('/api/general-ledger/validate',{...review,revision:'0'.repeat(64)})).status===409,'Stale capture revision rejected');await command(v,'validate',review);const post={proposal_id:proposal.proposal_id,revision:proposal.revision,reason:'Post selected industrial1800 once, no residual or stock entry.'},postKey=crypto.randomUUID(),j=await command(p,'post',post,postKey);ok((await command(p,'post',post,postKey)).replayed,'Posting receipt replay');ok((await command(p,'post',post)).journal_id===j.journal_id,'Other command key recovers same effect');ok((await p.request('/api/general-ledger/effect-prepare',body)).status===409,'No second industrial recognition'); d=await r.request(api);ok(d.status===200&&d.data.book.journals.length===1&&d.data.book.lines.length===2&&d.data.book.events.length===3&&d.data.industrial.accounting_state==='current','One industrial application, two lines and three events');view=await r.request('/general-ledger?industrial_lot_id='+fixture.lot_id);ok(view.status===200&&view.text.includes('/general-ledger/industrial?lot_id='+fixture.lot_id)&&view.text.includes('Lote #'),'Generic diary typed source label/link');const dates='from_on=2026-10-01&to_on=2026-10-31',account=fixture.accounts['QA-CUSTO-INDUSTRIAL'];d=await r.request('/api/general-ledger/account-ledger?'+dates+'&account_id='+account);ok(d.status===200&&d.data.closing_net_minor===180000&&d.data.rows[0].industrial_lot_id===String(fixture.lot_id),'Account ledger retains typed lot and exact1800');view=await r.request('/general-ledger/account-ledger?'+dates+'&account_id='+account);ok(view.status===200&&view.text.includes('/general-ledger/industrial?lot_id='+fixture.lot_id),'Account report links back to lot conference');ok((await p.request('/api/general-ledger/period-prepare',{period_key:'QA-2026-10',reason:'Other composed sources still need individual decisions.'})).status===409,'Industrial posting alone cannot close unresolved whole composition'); fs.writeFileSync(out,JSON.stringify({passed:true,checks,actual_http:true,actual_csrf:true,principal_minor:180000,residual_exact:'300',industrial_journals:1,industrial_lines:2,industrial_events:3,read_only_repeatable_read:true,writer_locks:0,typed_reports_and_links:true,visual_review:false},null,2)+'\n');console.log(JSON.stringify({passed:true,checks,industrial_journals:1})); }finally{server.kill();fs.closeSync(log);}