Files
stack/packages/cli/tests/gate-lane-fixtures.test.mjs
jason.woltje 1009bbfaf9 Hold closed fixture ports for the test's life (Vikunja #781)
leasePort({closed:true}) ignored its option: it probed an ephemeral port and
closed it, so a parallel test file could bind it and answer a request the
test expected to fail (preview.test.mjs:209 saw 200 instead of 502).

leaseClosedPort holds a bound, non-listening socket in a perl child until the
test's after hook. Connects still get ECONNREFUSED, bind() from another
process gets EADDRINUSE, and the kernel's ephemeral allocation skips the port.
Lane mode keeps its band and per-port lock, and releases the socket before
the lock. leasePort now refuses a closed option, so a stale call cannot get
the unheld port again.

Unanswerable callers (webui preview, serve, service) move to leaseClosedPort(t).
Callers that listen on the port themselves (stack-release rehearse, control-board
serve, tester-cli) keep leasePort and drop the ignored option.
2026-11-02 18:25:27 -06:00

144 lines
14 KiB
JavaScript

import test from 'node:test';import assert from 'node:assert/strict';
import { mkdtempSync,rmSync,mkdirSync,writeFileSync } from 'node:fs';import { join } from 'node:path';import { createServer,connect } from 'node:net';import { once } from 'node:events';
const api=await import('../../../scripts/testing/lane-fixtures.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
const evidence=await import('../../../scripts/testing/lane-evidence.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
test('parallel lease holders use only the active lane and its shared state',async t=>{
const ctx=leaseContext(t),lane=ctx.lane;
const [a,b]=await Promise.all([api.leasePort(ctx.fixture,ctx),api.leasePort(ctx.fixture,ctx)]);
const bands=BANDS,[lo,hi]=bands[lane];
assert.ok(a>=lo&&a<=hi);assert.ok(b>=lo&&b<=hi);assert.notEqual(a,b);
assert.ok(bands.a[1]<bands.b[0]);assert.equal(bands.a[1]-bands.a[0],127);assert.equal(bands.b[1]-bands.b[0],127);
});
test('unrelated home changes do not become lane writes, and own-root references do',()=>{
assert.equal(typeof evidence.attributeEntries,'function','attribution is required');
const result=evidence.attributeEntries([{path:'/home/person/.claude/projects/other-session',link:null},{path:'/home/person/.cache/lane-abcdef123456',link:null},{path:'/home/person/.pi/item',link:'/mnt/storage/scratch/gt-ABCDEF/work'}],{nonce:'abcdef123456',tempRoot:'/mnt/storage/scratch/gt-ABCDEF',fixturePaths:[]});
assert.equal(result.owned.length,2);assert.equal(result.unrelated.length,1);
});
test('proof summaries refuse missing timings, non-overlap and a pair without a lane swap',()=>{
const solos=[{candidate:'one',wallMs:100},{candidate:'two',wallMs:100}];
const make=(lane,candidate,startMs=0,endMs=120)=>({lane,candidate,startMs,endMs,rc:0,clean:true});
const pairs=[[make('a','one'),make('b','two',5,125)],[make('b','one'),make('a','two',5,125)],[make('a','one'),make('b','two',5,125)]];
const result=evidence.summarizePairs(solos,pairs);assert.equal(result.length,3);assert.equal(result[0].makespanMs,125);assert.equal(result[0].runs[0].slowdown,1.2);assert.equal(result[0].throughput,1.6);
const bad=structuredClone(pairs);delete bad[0][0].endMs;assert.throws(()=>evidence.summarizePairs(solos,bad),/timing/);
const noSwap=structuredClone(pairs);noSwap[1]=structuredClone(pairs[0]);assert.throws(()=>evidence.summarizePairs(solos,noSwap),/swap/);
const serial=structuredClone(pairs);serial[0][1].startMs=130;serial[0][1].endMs=250;assert.throws(()=>evidence.summarizePairs(solos,serial),/overlap/);
});
import { BANDS } from '../../../scripts/testing/lane-state.mjs';
import { spawn,spawnSync } from 'node:child_process';
import { readFileSync,existsSync } from 'node:fs';
function leaseContext(t){
const lane=process.env.MOSAIC_GATE_LANE??'a',stateRoot=process.env.MOSAIC_GATE_STATE??'/mnt/storage/scratch/mosaic-gate-lanes',callbacks=[];
mkdirSync(stateRoot,{recursive:true,mode:0o700});
t.after(async()=>{for(const f of callbacks.reverse())await f();});
return {lane,stateRoot,fixture:{after:f=>callbacks.push(f)},cleanup:f=>callbacks.push(f)};
}
const delay=ms=>new Promise(r=>setTimeout(r,ms));
async function waitFor(check){for(let i=0;i<100;i++){if(check())return;await delay(20);}throw Error('fixture not ready');}
test('an exhausted single-port band refuses, and an occupied foreign listener survives the probe',async t=>{
const ctx=leaseContext(t),callbacks=[];
const port=await api.leasePort({after:f=>callbacks.push(f)},ctx),original=BANDS[ctx.lane];
const server=createServer();
try{
BANDS[ctx.lane]=[port,port];
await assert.rejects(api.leasePort(t,ctx),/exhausted/);
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(port,'127.0.0.1',resolve);});
// Release only our lock. The sentinel listener remains foreign to the next lease.
await callbacks.pop()();
await assert.rejects(api.leasePort(t,ctx),/exhausted/);assert.equal(server.listening,true);
}finally{BANDS[ctx.lane]=original;if(server.listening)await new Promise(r=>server.close(r));for(const f of callbacks)await f();}
});
test('parallel child allocators share real lane locks; a lease remains held through child shutdown',async t=>{
const ctx=leaseContext(t),root=mkdtempSync('/mnt/storage/scratch/gf-');
const modulePath=new URL('../../../scripts/testing/lane-fixtures.mjs',import.meta.url).href;
const code=`import test from 'node:test';import fs from 'node:fs';const {leasePort}=await import(${JSON.stringify(modulePath)});test('lease held until fixture child finishes',async t=>{const port=await leasePort(t,JSON.parse(process.env.LEASE_CONTEXT));fs.writeFileSync(process.env.LEASE_PORT_FILE,String(port));await new Promise(r=>{const timer=setInterval(()=>{if(fs.existsSync(process.env.LEASE_STOP_FILE)){clearInterval(timer);r()}},5)});});`;
const start=number=>{const file=join(root,`lease-${number}.test.mjs`),portFile=join(root,`port-${number}`),stopFile=join(root,`stop-${number}`);writeFileSync(file,code);const env={...process.env,LEASE_CONTEXT:JSON.stringify(ctx),LEASE_PORT_FILE:portFile,LEASE_STOP_FILE:stopFile};delete env.NODE_TEST_CONTEXT;const child=spawn(process.execPath,['--test','--test-concurrency=2','--test-reporter=tap',file],{env,stdio:['ignore','pipe','pipe']});child.output='';child.stderr.on('data',b=>child.output+=b);child.stdout.on('data',b=>child.output+=b);child.done=new Promise(r=>child.once('exit',code=>{child.exitedCode=code;r(code);}));child.portFile=portFile;child.stopFile=stopFile;return child;};
const children=[start(1),start(2)];ctx.cleanup(async()=>{try{for(const c of children)writeFileSync(c.stopFile,'stop');await Promise.all(children.map(c=>c.done));}finally{rmSync(root,{recursive:true,force:true});}});
await waitFor(()=>{for(const c of children)if(c.exitedCode!==undefined)throw Error(`allocator exited ${c.exitedCode}: ${c.output}`);return children.every(c=>existsSync(c.portFile));});
const ports=children.map(c=>Number(readFileSync(c.portFile,'utf8')));assert.notEqual(...ports);
const lock=join(ctx.stateRoot,'ports',ctx.lane,`${ports[0]}.lock`);
assert.equal(spawnSync('flock',['-n',lock,'true']).status,1);
writeFileSync(children[0].stopFile,'stop');assert.equal(await children[0].done,0,children[0].output);
assert.equal(spawnSync('flock',['-n',lock,'true']).status,0);
assert.equal(spawnSync('flock',['-n',join(ctx.stateRoot,'ports',ctx.lane,`${ports[1]}.lock`),'true']).status,1);
});
test('abrupt allocator death releases its holder without touching a foreign lease',async t=>{
const ctx=leaseContext(t),foreign=await api.leasePort(ctx.fixture,ctx),modulePath=new URL('../../../scripts/testing/lane-fixtures.mjs',import.meta.url).href;
const code=`const {leasePort}=await import(${JSON.stringify(modulePath)});const port=await leasePort({after:()=>{}},JSON.parse(process.argv[1]));console.log(port);setInterval(()=>{},1000);`;
const child=spawn(process.execPath,['--input-type=module','-e',code,JSON.stringify(ctx)],{stdio:['ignore','pipe','pipe']});const done=new Promise(r=>child.once('exit',r));ctx.cleanup(async()=>{child.kill('SIGKILL');await done;});
const port=await new Promise(r=>child.stdout.once('data',b=>r(Number(String(b).trim()))));assert.notEqual(port,foreign);
child.kill('SIGKILL');await done;
const lock=join(ctx.stateRoot,'ports',ctx.lane,`${port}.lock`);
await waitFor(()=>spawnSync('flock',['-n',lock,'true']).status===0);
assert.equal(spawnSync('flock',['-n',join(ctx.stateRoot,'ports',ctx.lane,`${foreign}.lock`),'true']).status,1);
});
test('fixture caches survive a replacement environment with a separate tmp subdirectory',async t=>{
const root=mkdtempSync('/mnt/storage/scratch/gf-');t.after(()=>rmSync(root,{recursive:true,force:true}));
const env=api.fixtureCacheEnv(root);assert.equal(env.TMPDIR,join(root,'tmp'));assert.equal(env.JITI_FS_CACHE,join(root,'jiti'));assert.equal(env.XDG_CACHE_HOME,join(root,'cache'));
const code="const fs=require('fs'),p=require('path');for(const k of ['TMPDIR','JITI_FS_CACHE','XDG_CACHE_HOME'])fs.writeFileSync(p.join(process.env[k],k),'fixture');if(process.env.HOME)process.exit(7)";
const result=spawnSync(process.execPath,['-e',code],{env,encoding:'utf8'});assert.equal(result.status,0,result.stderr);
for(const key of Object.keys(env))assert.equal(readFileSync(join(env[key],key),'utf8'),'fixture');
});
test('an unrelated session can append while a supervised step runs without becoming an attributed write',async t=>{
const root=mkdtempSync('/mnt/storage/scratch/gf-');t.after(()=>rmSync(root,{recursive:true,force:true}));
const home=join(root,'home'),out=join(root,'out'),file=join(home,'.claude','other-session');mkdirSync(join(home,'.claude'),{recursive:true});mkdirSync(out);writeFileSync(file,'before');
const before=evidence.homeMetadata(home),prior=new Map(before.map(e=>[e.path,JSON.stringify(e)]));
const {executeSteps}=await import('../../../scripts/testing/gate-lane.mjs');
const barrier=join(root,'step-running');
const append=spawn(process.execPath,['-e',"const fs=require('fs');const deadline=setTimeout(()=>{console.error('append barrier timed out');process.exit(8)},5000);const timer=setInterval(()=>{if(fs.existsSync(process.argv[2])){clearInterval(timer);clearTimeout(deadline);fs.appendFileSync(process.argv[1],'unrelated append')}},5)",file,barrier],{stdio:'ignore'});const appended=new Promise(r=>append.once('exit',r));t.after(()=>append.kill('SIGKILL'));
let appendedDuringStep=false;
const stepCode="const fs=require('fs');fs.writeFileSync(process.argv[1],'running');const deadline=setTimeout(()=>{console.error('session append timed out');process.exit(8)},5000);const timer=setInterval(()=>{if(fs.readFileSync(process.argv[2],'utf8').includes('unrelated append')){clearInterval(timer);clearTimeout(deadline)}},5)";
const result=await executeSteps([{name:'wait',cwd:root,argv:[process.execPath,'-e',stepCode,barrier,file]}],{out,env:process.env,record:r=>{if(r.kind==='step')appendedDuringStep=readFileSync(file,'utf8').includes('unrelated append');}});
assert.equal(await appended,0);assert.equal(result.rc,0);assert.equal(appendedDuringStep,true);
const changed=evidence.homeMetadata(home).filter(e=>prior.get(e.path)!==JSON.stringify(e));assert.ok(changed.some(e=>e.path===file));
const attributed=evidence.attributeEntries(changed,{nonce:'abcdef123456',tempRoot:join(root,'temp'),fixturePaths:[]});assert.equal(attributed.owned.length,0);assert.ok(attributed.unrelated.length>0);
});
// Vikunja #781: a port that nothing answers on is held for the test's life, so a parallel file
// cannot bind it and answer a request the test expects to fail.
const connectCode=port=>new Promise(resolve=>{const c=connect(port,'127.0.0.1');c.once('error',e=>resolve(e.code));c.once('connect',()=>{c.destroy();resolve('connected');});});
const bindCode=(port,host='127.0.0.1')=>new Promise(resolve=>{const s=createServer();s.once('error',e=>resolve(e.code));s.listen(port,host,()=>s.close(()=>resolve('bound')));});
for(const mode of ['lane','no lane'])test(`a closed port lease refuses connects and stays held against binds, other leases and the kernel until release (${mode})`,async t=>{
const ctx=leaseContext(t),context=mode==='lane'?ctx:{lane:''},callbacks=[],fixture={after:f=>callbacks.push(f)};
const release=async()=>{for(const f of callbacks.splice(0).reverse())await f();};t.after(release);
const port=await api.leaseClosedPort(fixture,context),other=await api.leaseClosedPort(fixture,context),listener=await api.leasePort(fixture,context);
const lock=n=>join(ctx.stateRoot,'ports',ctx.lane,`${n}.lock`);
assert.equal(new Set([port,other,listener]).size,3);
if(mode==='lane'){const [lo,hi]=BANDS[ctx.lane];for(const n of [port,other,listener]){assert.ok(n>=lo&&n<=hi);assert.equal(spawnSync('flock',['-n',lock(n),'true']).status,1);}}
assert.equal(await connectCode(port),'ECONNREFUSED');
assert.equal(await bindCode(port),'EADDRINUSE');assert.equal(await bindCode(port,'0.0.0.0'),'EADDRINUSE');
if(mode==='no lane'){
const seen=new Set();
for(let i=0;i<200;i++){const s=createServer();await new Promise(r=>s.listen(0,'127.0.0.1',r));seen.add(s.address().port);await new Promise(r=>s.close(r));}
assert.equal(seen.has(port),false);assert.equal(seen.has(other),false);
}
await release();
assert.equal(await bindCode(port),'bound');assert.equal(await connectCode(port),'ECONNREFUSED');
if(mode==='lane')for(const n of [port,other,listener])assert.equal(spawnSync('flock',['-n',lock(n),'true']).status,0);
});
test('a closed port lease skips a locked port and a foreign listener, and refuses an exhausted band',async t=>{
const ctx=leaseContext(t),callbacks=[],original=BANDS[ctx.lane];
const port=await api.leasePort({after:f=>callbacks.push(f)},ctx),server=createServer();
try{
BANDS[ctx.lane]=[port,port];
await assert.rejects(api.leaseClosedPort(t,ctx),/exhausted/);
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(port,'127.0.0.1',resolve);});
await callbacks.pop()();
await assert.rejects(api.leaseClosedPort(t,ctx),/exhausted/);assert.equal(server.listening,true);
await new Promise(r=>server.close(r));
const got=await api.leaseClosedPort({after:f=>callbacks.push(f)},ctx);assert.equal(got,port);
assert.equal(await connectCode(port),'ECONNREFUSED');
}finally{BANDS[ctx.lane]=original;if(server.listening)await new Promise(r=>server.close(r));for(const f of callbacks.splice(0).reverse())await f();}
});
test('leasePort refuses the closed option it used to ignore',async()=>{
await assert.rejects(api.leasePort({after(){}},{closed:true}),/leaseClosedPort/);
});
test('abrupt owner death releases a closed port holder',async t=>{
const ctx=leaseContext(t),modulePath=new URL('../../../scripts/testing/lane-fixtures.mjs',import.meta.url).href;
const code=`const {leaseClosedPort}=await import(${JSON.stringify(modulePath)});const port=await leaseClosedPort({after:()=>{}},JSON.parse(process.argv[1]));console.log(port);setInterval(()=>{},1000);`;
const child=spawn(process.execPath,['--input-type=module','-e',code,JSON.stringify(ctx)],{stdio:['ignore','pipe','pipe']});const done=new Promise(r=>child.once('exit',r));ctx.cleanup(async()=>{child.kill('SIGKILL');await done;});
const port=await new Promise(r=>child.stdout.once('data',b=>r(Number(String(b).trim()))));
assert.equal(await bindCode(port),'EADDRINUSE');
child.kill('SIGKILL');await done;
let code2;for(let i=0;i<100&&code2!=='bound';i++){code2=await bindCode(port);if(code2!=='bound')await delay(20);}
assert.equal(code2,'bound');
});