← back to Tk11438 Postgres Migration

reverify.cjs

56 lines

// Read-only TK-11438 handoff verification. No app imports, writes, or restarts.
const fs=require('fs'),path=require('path'),cp=require('child_process'),assert=require('assert/strict');
const axon=require('/Users/macstudio3/.npm-global/lib/node_modules/pm2/node_modules/pm2-axon');
const rpc=require('/Users/macstudio3/.npm-global/lib/node_modules/pm2/node_modules/pm2-axon-rpc');
let rpcSocket;
const root='/Users/macstudio3/Projects';
const report={ticket:'TK-11438',timestamp:new Date().toISOString(),checks:[],services:[]};
const check=(name,ok,detail)=>{report.checks.push({name,verdict:ok?'PASS':'FAIL',detail});};
const target=value=>{try{const u=new URL(value);return {host:u.hostname,database:u.pathname.slice(1),socketHost:u.searchParams.get('host'),port:u.port||'5432'};}catch{return {unresolved:true};}};
let phase='PM2 existing daemon';
const timer=setTimeout(()=>{report.verdict='FAIL';report.error={code:'TIMEOUT',phase};fs.writeFileSync(path.join(__dirname,'verification/reverification.json'),JSON.stringify(report,null,2)+'\n');rpcSocket?.close();process.exit(2);},60000);
async function dbProbe(pg,config,table){
 const c=new pg.Client({...config,connectionTimeoutMillis:4000,application_name:'TK-11438-resume-readonly',options:'-c default_transaction_read_only=on -c statement_timeout=4000'});
 try{await c.connect();const identity=(await c.query("SELECT current_database() database,current_user role,inet_client_addr()::text client_addr,current_setting('transaction_read_only') readonly")).rows[0];
 const n=(await c.query('SELECT count(*)::int n FROM '+table)).rows[0].n;return {identity,count:n};}finally{await c.end().catch(()=>{});}
}
(async()=>{
 assert(fs.existsSync('/Users/macstudio3/.pm2/rpc.sock'));
 // Direct read-only RPC: no PM2 API initialization, module launch, or daemon start.
 const list=await new Promise((resolve,reject)=>{rpcSocket=axon.socket('req');const client=new rpc.Client(rpcSocket);rpcSocket.on('error',reject);rpcSocket.connect('/Users/macstudio3/.pm2/rpc.sock');client.call('getMonitorData',{},(err,data)=>{rpcSocket.close();err?reject(err):resolve(data);});});
 phase='service probes';
 const dump=JSON.parse(fs.readFileSync('/Users/macstudio3/.pm2/dump.pm2'));
 for(const s of list){const e=s.pm2_env,targets=[];for(const [key,value] of Object.entries(e)){if(typeof value==='string'&&/^postgres(?:ql)?:\/\//.test(value))targets.push({key,...target(value)});else if(['PGHOST','DB_HOST','PGDATABASE'].includes(key))targets.push({key,value});}
 report.services.push({name:s.name,pid:s.pid,status:e.status,cwd:e.pm_cwd,script:e.pm_exec_path,targets});}
 for(const name of ['interiordesignershowroom','patterndesignlab']){
 const s=list.filter(x=>x.name===name);assert.equal(s.length,1);const e=s[0].pm2_env,dir=path.join(root,name),showroom=name==='interiordesignershowroom';
 assert.equal(e.pm_cwd,dir);assert.equal(e.pm_exec_path,path.join(dir,'server.js'));assert.equal(e.status,'online');
 const local=require(path.join(dir,'node_modules/dotenv')).parse(fs.readFileSync(path.join(dir,'.env')));
 const env={...local,...e,...e.env};
 const saved=dump.filter(x=>x.name===name);assert.equal(saved.length,1);
 const source=fs.readFileSync(path.join(dir,showroom?'lib/db.js':'server.js'),'utf8');
 const pool=source.match(/new Pool\(([\s\S]*?)\);/);assert(pool);
 const config=require('vm').runInNewContext('('+pool[1]+')',{process:{env}},{timeout:1000});
 const pg=require(path.join(dir,'node_modules/pg'));
 const probe=await dbProbe(pg,config,showroom?'products':'designs');
 check(name+' source/effective DB identity and socket',probe.identity.database===(showroom?'idshowroom':'patterndesignlab')&&probe.identity.client_addr===null&&probe.identity.readonly==='on',probe);
 const savedEnv={...local,...saved[0],...saved[0].env};
 const durableConfig=require('vm').runInNewContext('('+pool[1]+')',{process:{env:savedEnv}},{timeout:1000});
 const durable=new pg.Client(durableConfig),live=new pg.Client(config);
 check(name+' durable host/db/role matches runtime',durable.connectionParameters.host==='/tmp'&&['host','database','user'].every(k=>durable.connectionParameters[k]===live.connectionParameters[k]),{host:durable.connectionParameters.host,database:durable.connectionParameters.database,role:durable.connectionParameters.user,reboot_tested:false});
 if(showroom)check(name+' durable URL fields and dotenv',saved[0].DATABASE_URL===e.DATABASE_URL&&saved[0].env.DATABASE_URL===e.DATABASE_URL&&local.DATABASE_URL===e.DATABASE_URL,{effective:target(e.DATABASE_URL),dump_top:target(saved[0].DATABASE_URL),dump_nested:target(saved[0].env.DATABASE_URL),dotenv:target(local.DATABASE_URL)});
 const bad=new pg.Client({host:'/tmp/TK-11438-missing-socket',database:probe.identity.database,connectionTimeoutMillis:1000});
 try{await bad.connect();check(name+' missing socket',false,'connected unexpectedly');}catch(err){check(name+' missing socket',err.code==='ENOENT',{code:err.code});}finally{await bad.end().catch(()=>{});}
 const user=env[showroom?'ADMIN_USER':'PDL_USER']||'admin',password=env[showroom?'ADMIN_PASS':'PDL_PASS']||'DW2024!';
 const base='http://127.0.0.1:'+(env.PORT||(showroom?9820:9781));
 const route=showroom?'/admin/brands':'/api/designs?limit=2&sort=sku_az';
 for(const authorized of [false,true]){const r=await fetch(base+route,{headers:authorized?{authorization:'Basic '+Buffer.from(user+':'+password).toString('base64')}:{},signal:AbortSignal.timeout(10000)});const body=await r.text();check(name+(authorized?' authenticated read':' auth boundary'),r.status===(authorized?200:401),{status:r.status,bytes:Buffer.byteLength(body)});}
 const readRoute=showroom?'/api/brands':'/api/healthz';const r=await fetch(base+readRoute,{signal:AbortSignal.timeout(10000)});await r.arrayBuffer();check(name+' DB-backed HTTP route',r.status===200,{route:readRoute,status:r.status});
 let tcp='';try{tcp=cp.execFileSync('lsof',['-nP','-a','-p',String(s[0].pid),'-iTCP'],{encoding:'utf8',timeout:5000});}catch(err){if(err.status!==1)throw err;tcp=err.stdout?.toString()||'';}
 check(name+' process has no TCP 5432',!/:5432\b/.test(tcp),{pid:s[0].pid});
 }
 const pg=require(path.join(root,'patterndesignlab/node_modules/pg'));const c=new pg.Client({host:'/tmp',database:'postgres',user:'macstudio3',connectionTimeoutMillis:4000,options:'-c default_transaction_read_only=on'});
 try{await c.connect();report.postgres=(await c.query("SELECT current_setting('listen_addresses') listen_addresses,current_setting('unix_socket_directories') socket_dirs")).rows[0];report.clients=(await c.query("SELECT datname,usename,application_name,client_addr::text,client_port,pid,state FROM pg_stat_activity WHERE backend_type='client backend' AND pid<>pg_backend_pid() ORDER BY datname,pid")).rows;}finally{await c.end().catch(()=>{});}
 report.verdict=report.checks.every(x=>x.verdict==='PASS')?'FIRST_BATCH_PASS_FULL_FLEET_BLOCKED':'FAIL';
})().catch(err=>{report.error={name:err.name,code:err.code};report.verdict='FAIL';process.exitCode=1;}).finally(()=>{clearTimeout(timer);fs.writeFileSync(path.join(__dirname,'verification/reverification.json'),JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify({verdict:report.verdict,checks:report.checks,postgres:report.postgres,clients:report.clients,error:report.error},null,2));});