Files
compras/scripts/sgf-sourcing-isolated.mjs
2026-11-02 20:27:11 +02:00

55 lines
8.9 KiB
JavaScript

import fs from 'node:fs';import path from 'node:path';import crypto from 'node:crypto';import {spawnSync,spawn} from 'node:child_process';import net from 'node:net';
const repo='C:/laragon/www/compras',parent='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939',source=parent+'/development-app';
const suffix=crypto.randomBytes(6).toString('hex'),database='sgf_sourcing_test_'+suffix,work=parent+'/private/sourcing-'+suffix,clone=work+'/app';
const pg='C:/Program Files/PostgreSQL/17/bin',php='C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe';fs.mkdirSync(work,{recursive:true});
const envText=fs.readFileSync(source+'/.env','utf8');const env=Object.fromEntries([...envText.matchAll(/^([A-Z_]+)=(.*)$/gm)].map(m=>[m[1],m[2].trim().replace(/^['"]|['"]$/g,'')]));
if(env.DB_DATABASE!=='sgf_modules_dev_2576427d1939'||env.DB_PORT!=='55439')throw Error('Wrong demonstration');
const admin=fs.readFileSync(parent+'/private/admin-password.txt','utf8');
function run(exe,args,options={}){const r=spawnSync(exe,args,{windowsHide:true,encoding:'utf8',maxBuffer:16*1024*1024,...options});if(r.status!==0)throw Error((r.stdout??'')+(r.stderr??''));return r.stdout;}
const adminEnv={...process.env,PGPASSWORD:admin};const qaEnv={...process.env,PGPASSWORD:env.DB_PASSWORD};
const fingerprintSQL="BEGIN READ ONLY;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM ir_requests t;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM cb_budget_entries t;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM users t;SELECT md5(coalesce(jsonb_agg(to_jsonb(t) ORDER BY id)::text,'')) FROM permission_grants t;COMMIT;";
const sourceFingerprint=(db=env.DB_DATABASE)=>run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',db,'-At','-v','ON_ERROR_STOP=1','-c',fingerprintSQL],{env:qaEnv});
const before=sourceFingerprint();let created=false,server;
try{
const archivedBaseline=parent+'/private/sourcing-recovery/before.dump',useArchivedBaseline=fs.existsSync(archivedBaseline);
if(useArchivedBaseline)fs.copyFileSync(archivedBaseline,work+'/before.dump');else run(pg+'/pg_dump.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',env.DB_DATABASE,'-Fc','-f',work+'/before.dump'],{env:qaEnv});
const cluster=run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-At','-c',"select current_setting('cluster_name')"],{env:adminEnv}).trim();if(cluster!=='sgf_modular_regression_2576427d1939')throw Error('Wrong cluster');
run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-v','ON_ERROR_STOP=1','-c',`CREATE DATABASE ${database} OWNER sgf_modules_qa_2576427d1939`],{env:adminEnv});created=true;
run(pg+'/pg_restore.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'--no-owner','--no-privileges','--exit-on-error',work+'/before.dump'],{env:qaEnv});
const copyBaselineFingerprint=sourceFingerprint(database);
fs.mkdirSync(clone,{recursive:true});
for(const dir of ['app','config','routes','bootstrap','scripts','packages','resources','public'])fs.cpSync(source+'/'+dir,clone+'/'+dir,{recursive:true});
const guard=clone+'/app/Shared/Testing/TestingDatabaseGuard.php';fs.writeFileSync(guard,fs.readFileSync(guard,'utf8').replaceAll('sgf_modules_dev_2576427d1939',database)); // Exact physical identity adaptation; guards remain enabled.
for(const file of ['artisan','composer.json','composer.lock'])fs.copyFileSync(source+'/'+file,clone+'/'+file);fs.cpSync(source+'/storage/app/private/module-deployments',clone+'/storage/app/private/module-deployments',{recursive:true});
for(const d of ['storage/framework/sessions','storage/framework/views','storage/framework/cache/data','storage/logs'])fs.mkdirSync(clone+'/'+d,{recursive:true});
fs.cpSync(source+'/vendor',clone+'/vendor',{recursive:true}); // Independent autoloader and dependencies; never mutate the demo vendor.
for(const dir of fs.readdirSync(clone+'/storage/app/private/module-deployments',{withFileTypes:true})){if(!dir.isDirectory())continue;const file=clone+'/storage/app/private/module-deployments/'+dir.name+'/journal.json';if(fs.existsSync(file)){const j=JSON.parse(fs.readFileSync(file));j.host_identity=crypto.createHash('sha256').update(clone).digest('hex');fs.writeFileSync(file,JSON.stringify(j,null,2));}}
if(useArchivedBaseline){
const expectedBaseline=JSON.parse(fs.readFileSync(repo+'/docs/proofs/sgf-read-candidate.json','utf8')).archive.manifest_sha256;let hops=0;fs.writeFileSync(clone+'/storage/framework/down','{}');
while(true){const current=JSON.parse(fs.readFileSync(clone+'/storage/app/private/module-deployments/current.json','utf8'));if(current.manifest_sha256===expectedBaseline)break;if(++hops>4)throw Error('Unexpected baseline ancestry');const rolled=JSON.parse(run(php,[repo+'/scripts/sgf-code-deploy.php','rollback',clone,current.deployment_id,'Disposable reproduction of pre-sourcing baseline','--offline'],{cwd:clone}));if(rolled.state!=='rolled-back')throw Error('Failed to reproduce prior code');}fs.unlinkSync(clone+'/storage/framework/down');
}
fs.writeFileSync(clone+'/.env',envText.replace(/^DB_DATABASE=.*$/m,'DB_DATABASE='+database).replace(/^APP_ENV=.*$/m,'APP_ENV=testing').replace(/^APP_DEBUG=.*$/m,'APP_DEBUG=false'));
const vars={...process.env,SOURCING_CLONE:clone,SOURCING_DB:database,SOURCING_WORK:work,SOURCING_REPO:repo,APP_BASE_PATH:clone};
const setup=run(php,[repo+'/tests/Support/sgf-sourcing-copy.php','setup'],{cwd:clone,env:vars});
fs.writeFileSync(work+'/setup-output.txt',setup);let receipt;try{receipt=JSON.parse(setup);}catch{throw Error('Setup failed; private diagnostic '+work+'/setup-output.txt');}if(!receipt.passed)throw Error('Setup proof did not pass');
const listener=net.createServer();await new Promise(r=>listener.listen(0,'127.0.0.1',r));const port=listener.address().port;await new Promise(r=>listener.close(r));
server=spawn(php,['-S','127.0.0.1:'+port,'-t','public','scripts/router.php'],{cwd:clone,env:vars,windowsHide:true,stdio:['ignore',fs.openSync(work+'/web-out.log','a'),fs.openSync(work+'/web-error.log','a')]});
await new Promise((resolve,reject)=>{let tries=0;const poll=()=>{const s=net.connect(port,'127.0.0.1');s.once('connect',()=>{s.destroy();resolve();});s.once('error',()=>{s.destroy();if(++tries>50)reject(Error('Test web host unavailable'));else setTimeout(poll,100);});};poll();});
const http=run(process.execPath,[repo+'/tests/Support/sgf-sourcing-http.mjs'],{cwd:repo,env:{...vars,SOURCING_PORT:String(port)}});fs.writeFileSync(work+'/http-output.txt',http);
const result=JSON.parse(http);if(!result.passed)throw Error('HTTP test failed');
server.kill();await new Promise(r=>setTimeout(r,200));server=null;
const recovered=JSON.parse(run(php,[repo+'/tests/Support/sgf-sourcing-copy.php','verify'],{cwd:clone,env:vars}));
// Data/schema recovery is separate from the successful code rollback above.
run(pg+'/pg_dump.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'-Fc','-f',work+'/confirmed-before-recovery.dump'],{env:qaEnv});
run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-v','ON_ERROR_STOP=1','-c',`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${database}' AND pid<>pg_backend_pid()`,'-c',`DROP DATABASE ${database}`,'-c',`CREATE DATABASE ${database} OWNER sgf_modules_qa_2576427d1939`],{env:adminEnv});
run(pg+'/pg_restore.exe',['-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'--no-owner','--no-privileges','--exit-on-error',work+'/before.dump'],{env:qaEnv});
if(sourceFingerprint(database)!==copyBaselineFingerprint)throw Error('Restored source request, reservation, accounts or grants diverged');
const schemaRestored=run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',env.DB_USERNAME,'-d',database,'-At','-c',"SELECT count(*) FROM pg_tables WHERE schemaname='public' AND tablename LIKE 'ss_%'; SELECT count(*) FROM sgc_installations WHERE module='sourcing'"],{env:qaEnv}).trim();if(schemaRestored!=='0\n0'&&schemaRestored!=='0\r\n0')throw Error('Schema recovery diverged');
recovered.data_schema_restore_verified=true;recovered.pre_recovery_state_archived=true;recovered.restored_requests_reservations_accounts_grants_match=true;
const proof={passed:true,database,work,setup:receipt,http:result,recovery:recovered,persistent_demo_unchanged:before===sourceFingerprint(),synthetic_copy_only:true,checked_at:new Date().toISOString()};if(!proof.persistent_demo_unchanged)throw Error('Original demo changed');
fs.writeFileSync(repo+'/docs/proofs/sgf-sourcing-isolated.json',JSON.stringify(proof,null,2));console.log(JSON.stringify(proof));
}finally{
if(server)server.kill();
if(created)run(pg+'/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U','sgf_regression_owner','-d','postgres','-v','ON_ERROR_STOP=1','-c',`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${database}' AND pid<>pg_backend_pid()`,'-c',`DROP DATABASE ${database}`],{env:adminEnv});
}