import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import {spawnSync} from 'node:child_process'; const state=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json','utf8')); if(!/^[a-f0-9]{12}$/.test(state.id)||path.resolve(state.root)!==path.resolve('C:/laragon/data/sgf-tempo/run/modular-regression-'+state.id)||state.port!==55439)throw Error('Unexpected QA cluster'); const resume=process.argv[2]==='new'?undefined:process.argv[2],step=process.argv[3]||'linked';if(resume&&!/^[a-f0-9]{12}$/.test(resume))throw Error('Bad resume tag'); const selectedFilter=process.argv[4];if(selectedFilter&&!/^[a-zA-Z0-9_ |]+$/.test(selectedFilter))throw Error('Invalid targeted test filter'); if(['financial-read-affected','native-payable-compat'].includes(step)&&resume)throw Error('Financial RefreshDatabase cases require a new owned QA database; retained proof databases are never reset.'); if(!['linked','affected','lots','replay','issue-affected','issue-lots','return-native','return-link','return-reversals','return-flow','return-extra','financial-read-affected','native-payable-page','native-payable-compat'].includes(step))throw Error('Unknown isolated receipt/issue step'); const tag=resume??crypto.randomBytes(6).toString('hex'),root=path.join(state.root,'private','stock-receipt-'+tag),app=path.join(root,'app'); const source=path.resolve('../compras'),database='compras_receipts_test_'+tag,role='stock_receipt_qa_'+tag,reader='stock_receipt_read_'+tag; const php='C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',psql='C:/Program Files/PostgreSQL/17/bin/psql.exe'; const admin=fs.readFileSync(path.join(state.root,'private','admin-password.txt'),'utf8').trim(); const pg=(input,roleName='sgf_regression_owner',db='postgres',password=admin)=>{ const r=spawnSync(psql,['-h','127.0.0.1','-p','55439','-U',roleName,'-d',db,'-At','-v','ON_ERROR_STOP=1'],{encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:password},input}); if(r.status!==0)throw Error('Isolated PostgreSQL operation failed; inspect private QA state without changing any destination');return r.stdout.trim(); }; if(pg("SELECT current_setting('cluster_name');")!=='sgf_modular_regression_'+state.id)throw Error('Wrong QA cluster'); fs.mkdirSync(app,{recursive:true}); if(!resume){ const password=crypto.randomBytes(32).toString('hex'),readPassword=crypto.randomBytes(32).toString('hex'); pg(`CREATE ROLE ${role} LOGIN PASSWORD '${password}' NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; CREATE DATABASE ${database} OWNER ${role}; REVOKE ALL ON DATABASE ${database} FROM PUBLIC; CREATE ROLE ${reader} LOGIN PASSWORD '${readPassword}' NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; ALTER ROLE ${reader} SET default_transaction_read_only=on; GRANT CONNECT ON DATABASE ${database} TO ${reader};`); fs.writeFileSync(path.join(root,'state.json'),JSON.stringify({tag,root,app,database,role,reader,cluster:'sgf_modular_regression_'+state.id},null,2)); fs.writeFileSync(path.join(root,'source-connection.json'),JSON.stringify({driver:'pgsql',host:'127.0.0.1',port:55439,database,username:reader,password:readPassword,charset:'utf8',prefix:'',search_path:'public',sslmode:'prefer'})); fs.writeFileSync(path.join(app,'.env.testing'),`APP_ENV=testing\nAPP_KEY=base64:${crypto.randomBytes(32).toString('base64')}\nAPP_DEBUG=false\nAPP_URL=http://localhost\nDB_CONNECTION=pgsql\nDB_HOST=127.0.0.1\nDB_PORT=55439\nDB_DATABASE=${database}\nDB_USERNAME=${role}\nDB_PASSWORD=${password}\nDB_URL=\nSESSION_DRIVER=array\nCACHE_STORE=array\nQUEUE_CONNECTION=sync\nBCRYPT_ROUNDS=4\nSGF_ISOLATED_REGRESSION_RUN=${state.id}\nCOMMON_PAYABLES_ENABLED=false\nPAYABLE_SOURCE_INSTANCE=\n`); }else{ const old=JSON.parse(fs.readFileSync(path.join(root,'state.json'),'utf8')); if(old.database!==database||old.role!==role||path.resolve(old.app)!==path.resolve(app))throw Error('Unexpected resume state'); } for(const dir of ['app','bootstrap','config','routes','resources','tests','database'])fs.cpSync(path.join(source,dir),path.join(app,dir),{recursive:true,filter:p=>!p.includes('bootstrap'+path.sep+'cache')}); for(const pkg of ['inventory-read','inventory-write','inventory-issue','inventory-return','purchasing','budget-control','payables'])fs.cpSync(path.join(source,'packages',pkg),path.join(app,'packages',pkg),{recursive:true}); if(!resume)fs.cpSync(path.join(source,'public/build'),path.join(app,'public/build'),{recursive:true}); fs.copyFileSync(path.join(source,'composer.json'),path.join(app,'composer.json')); if(!fs.existsSync(path.join(app,'vendor')))fs.symlinkSync(path.join(source,'vendor'),path.join(app,'vendor'),'junction'); for(const dir of ['bootstrap/cache','storage/logs','storage/framework/cache/data','storage/framework/sessions','storage/framework/views'])fs.mkdirSync(path.join(app,dir),{recursive:true}); const config=path.join(app,'phpunit-stock.xml'); fs.writeFileSync(config,``); const firstOutput=path.join(root,step+'-junit.xml');const output=fs.existsSync(firstOutput)?path.join(root,step+'-'+Date.now()+'-junit.xml'):firstOutput; const pest=[path.join(source,'vendor/pestphp/pest/bin/pest'),'--test-directory',path.relative(source,path.join(app,'tests')),'-c',config]; const args=step==='return-reversals'?[...pest,path.join(app,'tests/Feature/InventoryReversalsTest.php')]:step==='issue-affected'?[...pest,path.join(app,'tests/Feature/StockOperationsTest.php')]:step==='issue-lots'?[...pest,path.join(app,'tests/Feature/InventoryLotsTest.php'),'--filter','FEFO consumes|expiry and hold']:step==='affected'?[...pest,path.join(app,'tests/Feature/ReceiptStockTest.php')]:step==='lots'?[...pest,path.join(app,'tests/Feature/InventoryLotsTest.php'),path.join(app,'tests/Feature/InventoryLotPolicyTest.php'),'--filter','tracked receipt|preapproved exemption|switching receipt policy']:[path.join(source,'vendor/phpunit/phpunit/phpunit'),'-c',config,path.join(app,'tests/Feature/'+(['return-native','return-link','return-flow','return-extra'].includes(step)?'WarehouseReturnIntegrationTest.php':step==='replay'?'ReceiptAdapterReplayTest.php':'CommonReceiptStockTest.php'))]; const selectedArgs=['financial-read-affected','native-payable-compat'].includes(step)?[...pest,...(step==='native-payable-compat'?[path.join(app,'tests/Feature/SupplierPayableIntegrationTest.php')]:[]),path.join(app,'tests/Feature/PayableHistoryImportTest.php'),path.join(app,'tests/Feature/PayablePaymentDecisionTest.php')]:step==='native-payable-page'?[path.join(source,'vendor/phpunit/phpunit/phpunit'),'-c',config,path.join(app,'tests/Feature/NativePayablePageTest.php')]:args; const r=spawnSync(php,[...selectedArgs,...(selectedFilter?['--filter',selectedFilter]:step==='return-link'?['--filter','native_receipt_issue|source_options']:step==='return-extra'?['--filter','competing_native|expired_and_maintenance']:step==='return-flow'?['--filter','test_(native_receipt|stale_capacity|decision_failure|undeclared|source_issue|concurrent_native|configured_context|return_signals)']:[]),'--log-junit',output],{cwd:app,stdio:'inherit',windowsHide:true,env:{...process.env,APP_BASE_PATH:app,APP_ENV:'testing',SGF_ISOLATED_REGRESSION_RUN:state.id,STOCK_RECEIPT_ROOT:root,...(['financial-read-affected','native-payable-page','native-payable-compat'].includes(step)?{COMMON_PAYABLES_PATH:path.resolve('packages/payables')}:{} )}}); if(r.status===0&&['linked','return-link','return-flow'].includes(step))pg(`GRANT USAGE ON SCHEMA public TO ${reader}; GRANT SELECT ON stock_items,warehouses,stock_movements TO ${reader};`, 'sgf_regression_owner',database); console.log(JSON.stringify({exit_code:r.status,tag,step,junit:output,isolated_database:database,root,reader_select_granted:r.status===0&&['linked','return-link','return-flow'].includes(step)}));process.exitCode=r.status??1;