Files
stack/packages/cli/live/test-support.mjs

65 lines
3.6 KiB
JavaScript

// Focused test fixtures only; runtime CLIs never import this file.
import { mkdtempSync, mkdirSync, writeFileSync, rmSync,readFileSync,readdirSync,readlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
export function fixture(t) {
if (!process.env.TMPDIR?.startsWith('/mnt/storage/scratch/tmp/') || process.env.TMPDIR.includes('_')) throw new Error('set TMPDIR with mktemp -d /mnt/storage/scratch/tmp/XXXXXX');
const root=mkdtempSync(join(tmpdir(),'ui797-'));
const cleanups=[];
t.after(async()=>{
const owned=ownedProcesses(root);
const errors=[];
for(const cleanup of cleanups.reverse()) try { await cleanup(); } catch(e) { errors.push(e); }
// Preserve evidence if resource shutdown failed; never delete beneath a running service.
const survivors=owned.filter(p=>processAlive(p));
if(survivors.length) errors.push(new Error(`owned processes survived fixture shutdown: ${survivors.map(p=>p.pid)}`));
if(errors.length) throw new AggregateError(errors,'fixture shutdown failed');
rmSync(root,{recursive:true,force:true});
});
const configDir=join(root,'mosaic-ui797'),dataRoot=join(root,'data');
mkdirSync(configDir,{mode:0o700});mkdirSync(dataRoot,{mode:0o700});
const config=join(configDir,'config.json');
writeFileSync(config,JSON.stringify({configVersion:1,environment:'development',dataRoot,execution:{backend:'docker',provider:'zai',model:'glm-5.3-flash'}}));
writeFileSync(join(dataRoot,'instance.json'),JSON.stringify({instanceVersion:1,name:'ui797'}));
return {root,config,dataRoot,after:fn=>cleanups.push(fn)};
}
export async function closeServer(server) {
const closed=new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));
server.closeAllConnections();
await closed;
}
export async function closeChild(child) {
if(child.exitCode!==null || child.signalCode!==null) return;
const closed=new Promise(resolve=>child.once('close',resolve));
child.kill('SIGTERM');
await closed;
}
// Snapshot only this fixture's processes, including reparented Chromium crash handlers.
export function ownedProcesses(root) {
const processes=readdirSync('/proc').filter(n=>/^\d+$/.test(n)).flatMap(pid=>{
try {
const args=readFileSync(`/proc/${pid}/cmdline`,'utf8').split('\0');
const stat=readFileSync(`/proc/${pid}/stat`,'utf8').split(' ');
return [{pid:Number(pid),ppid:Number(stat[3]),start:stat[21],args}];
} catch { return []; }
});
const selected=new Set(processes.filter(p=>p.args.some(a=>a===root || a.startsWith(root+'/') || a.startsWith('--user-data-dir='+root+'/'))).map(p=>p.pid));
// Chromium reports PIDs in its own namespace. Never mistake one for a host
// PID (which could belong to another seat) when tracking orphan crash handlers.
for(const p of processes) if(selected.has(p.pid)) for(const a of p.args) if(a.startsWith('--crashpad-handler-pid=')) {
for(const candidate of processes) try {
if(readlinkSync(`/proc/${candidate.pid}/ns/pid`)!==readlinkSync(`/proc/${p.pid}/ns/pid`))continue;
const ids=readFileSync(`/proc/${candidate.pid}/status`,'utf8').match(/^NSpid:\s+(.+)$/m)?.[1].trim().split(/\s+/);
if(Number(ids?.at(-1))===Number(a.split('=')[1]))selected.add(candidate.pid);
} catch {}
}
let size;
do {size=selected.size;for(const p of processes) if(selected.has(p.ppid))selected.add(p.pid);} while(size!==selected.size);
return processes.filter(p=>selected.has(p.pid));
}
export function processAlive(p) {
try { const stat=readFileSync(`/proc/${p.pid}/stat`,'utf8').split(' ');return stat[21]===p.start && stat[2]!=='Z'; } catch {return false;}
}