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,'composition-http.json');if(fs.existsSync(out)||!fs.existsSync(path.join(root,'ledger-composition-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;}};}; try{ const p=client(),v=client(),r=client(),tech=client();let ready=false;for(let n=0;n<50&&!ready;n++){try{ready=(await r.request('/api/general-ledger/workspace')).status===401;}catch{}if(!ready)await new Promise(res=>setTimeout(res,100));}ok(ready,'Anonymous workspace denied');await p.enter('prepare');await v.enter('validate');await r.enter('read');await tech.enter('technician');ok((await tech.request('/api/general-ledger/workspace')).status===403,'Technician does not inherit ledger access'); let d=await r.request('/api/general-ledger/workspace',undefined,undefined,{'X-Cost-QA-Proof':'read'});ok(d.status===200&&d.data.all_current_source_views&&d.data.book.accounts.length===0,'All bounded source panels and no account catalogue preload');ok(d.headers.get('cache-control').includes('no-store'),'Private workspace not cached');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,'One coherent read-only transaction without writer locks');for(const panel of ['sales','costs','suppliers'])ok(d.data.panels[panel].rows.length===1&&d.data.panels[panel].pagination.per_page===25,'One actual source in '+panel);const closed=d.data.period.status==='closed'; let view=await p.request('/general-ledger/workspace');ok(view.status===200,'Unified Blade view renders');for(const link of ['/general-ledger/sales?invoice_id=','/general-ledger/costs?order_id=','/general-ledger/flow?source='])ok(view.text.includes(link),'Workflow origin link '+link);ok(view.text.includes('data-action="period-prepare"')!==closed,'Period command appears only in open preparer view');fs.writeFileSync(path.join(root,'composition-preparer.html'),view.text);view=await r.request('/general-ledger/workspace');ok(view.status===200&&!view.text.includes('data-action='),'Reader has no financial command forms'); for(const query of ['invoices_page=0','costs_page=100001','suppliers_page%5Bx%5D=1','company_id=9','journals_page=1&unknown=1'])ok((await r.request('/api/general-ledger/workspace?'+query)).status===422,'Malformed or foreign-scope query rejected: '+query); d=await r.request('/api/general-ledger/workspace?costs_page=2&invoices_page=1');ok(d.status===200&&d.data.panels.costs.rows.length===0&&d.data.panels.sales.rows.length===1,'Cost page independent of invoice page'); const body={period_key:'QA-2026-10',reason:'Explicit joint conference requires every current source.'};ok((await p.request('/api/general-ledger/period-prepare',body,undefined,{'X-CSRF-TOKEN':''})).status===419,'CSRF enforced');ok((await r.request('/api/general-ledger/period-prepare',body)).status===403,'Reader cannot prepare period');if(!closed)ok((await p.request('/api/general-ledger/period-prepare',body)).status===409,'Unresolved cross-source state blocks conference through HTTP'); const old=await p.module('finance');ok((await p.request('/api/general-ledger/workspace')).status===409,'Accounting module context required');await p.module('accounting');ok((await p.request('/api/general-ledger/period-prepare',body,undefined,{'X-SGF-Context':old})).status===409,'Stale context generation refused'); const guide=await r.request('/qa/cost-demo');ok(guide.status===200&&guide.text.includes('Percurso financeiro composto QA'),'Composition guide replaces standalone fixture guide only in new composition'); fs.writeFileSync(out,JSON.stringify({passed:true,checks,actual_http:true,actual_csrf:true,period_closed:closed,read_only_repeatable_read:true,writer_locks:0,independent_pagination:true,visual_review:false},null,2)+'\n');console.log(JSON.stringify({passed:true,checks,period_closed:closed})); }finally{server.kill();fs.closeSync(log);}