import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs'; import { execFileSync, spawnSync } from 'node:child_process'; import { join } from 'node:path'; const api = await import('../../../scripts/testing/gate-lane.mjs').catch(e => { if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e; return {}; }); function fixture(t) { const root = mkdtempSync('/mnt/storage/scratch/gt-'); // Node appends six characters, keeping this root at 30 bytes. t.after(() => rmSync(root, { recursive: true, force: true })); const checkout = join(root, 'checkout'); mkdirSync(checkout); for (const [f,text] of [['package.json','{}'],['RELEASE','0.0.1\n']]) writeFileSync(join(checkout,f),text); execFileSync('git',['init','-q',checkout]); execFileSync('git',['-C',checkout,'add','.']); execFileSync('git',['-C',checkout,'-c','user.name=Fixture','-c','user.email=f@invalid','commit','-qm','fixture']); symlinkSync('/mnt/storage/src/mosaic-stack/node_modules',join(checkout,'node_modules')); return {lane:'a',candidate:execFileSync('git',['-C',checkout,'rev-parse','HEAD'],{encoding:'utf8'}).trim(),checkout,out:join(root,'out'),tempRoot:root,stateRoot:join(root,'state'),grant:'fixture-grant',hostFacts:{unprivilegedPortStart:1024,ephemeral:[32768,60999],listeners:[]}}; } test('preflight refuses unsafe lane roots, missing grants and live port 9 before side effects', t => { assert.equal(typeof api.validateRun,'function','validateRun is required'); const f=fixture(t); assert.equal(api.validateRun(f).lane,'a'); for(const [patch,reason] of [[{lane:'c'},/lane/],[{grant:''},/grant/],[{tempRoot:'/tmp'},/tmp/],[{tempRoot:join(f.tempRoot,'deeper')},/30|missing/],[{candidate:'a'.repeat(40)},/candidate/],[{hostFacts:{...f.hostFacts,listeners:[{address:'0.0.0.0',port:9}]}},/port.9/],[{hostFacts:{...f.hostFacts,unprivilegedPortStart:0}},/privileged/]]) assert.throws(()=>api.validateRun({...f,...patch}),reason); const link=join(f.tempRoot,'link');symlinkSync('/tmp',link);assert.throws(()=>api.validateRun({...f,tempRoot:link}),/tmp/); writeFileSync(join(f.checkout,'RELEASE'),'changed');assert.throws(()=>api.validateRun(f),/dirty/); }); test('discovery preserves all package and script steps while ignoring non-file shell names', t => { const f=fixture(t);mkdirSync(join(f.checkout,'packages')); for(const p of ['z','queue','a']){mkdirSync(join(f.checkout,'packages',p,'tests'),{recursive:true});writeFileSync(join(f.checkout,'packages',p,'tests','one.test.mjs'),'');} mkdirSync(join(f.checkout,'scripts'));writeFileSync(join(f.checkout,'scripts','test-z.sh'),'');writeFileSync(join(f.checkout,'scripts','test-a.sh'),'');mkdirSync(join(f.checkout,'scripts','test-directory.sh')); const steps=api.discoverSteps(f.checkout); assert.deepEqual(steps.map(s=>s.name),['pkg-a','pkg-z','script-a','script-z']); assert.deepEqual(steps[0].argv,['node','--test','--test-concurrency=2','tests/one.test.mjs']); }); test('a failed step stops the gate and records its nonzero exit without running the next step',async t=>{ assert.equal(typeof api.executeSteps,'function','executeSteps is required'); const f=fixture(t),events=[],out=f.out;mkdirSync(out);const marker=join(f.tempRoot,'should-not-exist'); const result=await api.executeSteps([{name:'first',cwd:f.checkout,argv:['bash','-c','exit 7']},{name:'second',cwd:f.checkout,argv:['touch',marker]}],{out,env:process.env,record:r=>events.push(r)}); assert.equal(result.rc,7);assert.equal(events.filter(e=>e.kind==='step').length,1);assert.ok(events.find(e=>e.kind==='step').wallMs>=0); const {existsSync}=await import('node:fs');assert.equal(existsSync(marker),false); }); test('a cancelled step stops its process group and records failure',async t=>{ const f=fixture(t);mkdirSync(f.out);const controller=new AbortController(),events=[]; const child=new URL('./fixtures/gate-lane-child.mjs',import.meta.url).pathname; const result=await api.executeSteps([{name:'child',cwd:f.checkout,argv:[process.execPath,child,'wait',join(f.tempRoot,'started')]}],{out:f.out,env:process.env,record:e=>{events.push(e);if(e.kind==='child')setTimeout(()=>controller.abort(),100);},signal:controller.signal}); assert.notEqual(result.rc,0);const owned=events.find(e=>e.kind==='child').owner;const {sameProcess}=await import('../../../scripts/testing/lane-state.mjs');assert.equal(sameProcess(owned),false); }); test('step receipts retain pass fail skipped counts and hashes of their actual logs',async t=>{ const f=fixture(t);mkdirSync(f.out);const events=[]; await api.executeSteps([{name:'counts',cwd:f.checkout,argv:[process.execPath,'-e',"console.log('ℹ pass 2\\nℹ fail 0\\nℹ skipped 1')"]}],{out:f.out,env:process.env,record:r=>events.push(r)}); const step=events.find(e=>e.kind==='step');assert.deepEqual(step.counts,{pass:2,fail:0,skipped:1});assert.match(step.logSha256,/^[a-f0-9]{64}$/); }); test('preflight rejects a real 31-byte root, missing dependency tree, occupied band and missing inventory',t=>{ const f=fixture(t),long=f.tempRoot+'x';mkdirSync(long,{mode:0o700});t.after(()=>rmSync(long,{recursive:true,force:true}));assert.equal(Buffer.byteLength(long),31); assert.throws(()=>api.validateRun({...f,tempRoot:long}),/30 bytes/); assert.throws(()=>api.validateRun({...f,hostFacts:{...f.hostFacts,ephemeral:[60000,62000]}}),/overlaps/); assert.throws(()=>api.validateRun({...f,hostFacts:{...f.hostFacts,listeners:[{address:'127.0.0.1',port:61005}]}}),/live listener/); assert.throws(()=>api.validateRun({...f,hostFacts:{}}),/inventory/); rmSync(join(f.checkout,'node_modules'));assert.throws(()=>api.validateRun(f),/node_modules/); }); test('preflight refuses canonical and builder checkouts and an existing output',t=>{ const f=fixture(t); for(const checkout of ['/mnt/storage/src/mosaic-stack',new URL('../../../',import.meta.url).pathname])assert.throws(()=>api.validateRun({...f,checkout}),/canonical or builder/); mkdirSync(f.out);assert.throws(()=>api.validateRun(f),/output already exists/); }); test('both candidates must carry every approved launcher file',t=>{ const f=fixture(t),other=join(f.tempRoot,'second');mkdirSync(other); for(const checkout of [f.checkout,other]){ const target=join(checkout,'scripts','testing');fsExtra.cpSync(new URL('../../../scripts/testing',import.meta.url),target,{recursive:true}); api.validateTooling(checkout); } fsExtra.appendFileSync(join(other,'scripts/testing/lane-state.mjs'),'\n// changed\n'); assert.throws(()=>api.validateTooling(other),/tooling differs/); fsExtra.rmSync(join(f.checkout,'scripts/testing/lane-fixtures.mjs')); assert.throws(()=>api.validateTooling(f.checkout),/tooling differs/); }); test('the 92-byte socket example passes and an over-107-byte pathname refuses',()=>{ const root='/mnt/storage/scratch/gt-ABCDEF';assert.equal(Buffer.byteLength(root),30); const socket=root+'/mosaic-cli-ABCDEF/data/launches/acme/r0123456789ab/tools.sock'; assert.equal(Buffer.byteLength(socket),92);assert.equal(stateApi.socketPath(socket),socket); assert.throws(()=>stateApi.socketPath(socket+'x'.repeat(16)),/107 bytes/); }); test('package/script duplicates stay and every supervised step clears NODE_TEST_CONTEXT',async t=>{ const f=fixture(t);mkdirSync(join(f.checkout,'packages/cli/tests'),{recursive:true});mkdirSync(join(f.checkout,'scripts')); writeFileSync(join(f.checkout,'packages/cli/tests/repeated.test.mjs'),'');writeFileSync(join(f.checkout,'scripts/test-cli.sh'),'node --test packages/cli/tests/repeated.test.mjs\n'); const steps=api.discoverSteps(f.checkout);assert.deepEqual(steps.map(s=>s.name),['pkg-cli','script-cli']); const original=process.env.NODE_TEST_CONTEXT;process.env.NODE_TEST_CONTEXT='parent-context'; let env;try{env=api.cleanEnv(f,{project:'fixture'},'/fake/docker');}finally{if(original===undefined)delete process.env.NODE_TEST_CONTEXT;else process.env.NODE_TEST_CONTEXT=original;} assert.equal(env.NODE_TEST_CONTEXT,undefined);assert.equal(env.HOME,process.env.HOME); mkdirSync(f.out);const events=[]; for(const s of steps)s.argv=[process.execPath,'-e',"if(process.env.NODE_TEST_CONTEXT)process.exit(9);console.log('cleared')"]; const result=await api.executeSteps(steps,{out:f.out,env,record:e=>events.push(e)}); assert.equal(result.rc,0);assert.equal(events.filter(e=>e.kind==='step').length,2); }); test('scoped ss loopback and wildcard port 9 listeners refuse',t=>{ const f=fixture(t); for(const address of ['127.0.0.1%lo','0.0.0.0%lo','::%lo','::ffff:127.0.0.1%lo'])assert.throws(()=>api.validateRun({...f,hostFacts:{...f.hostFacts,listeners:[{address,port:9}]}}),/port 9/); }); import * as fsExtra from 'node:fs'; import * as stateApi from '../../../scripts/testing/lane-state.mjs'; test('step children use legacy umask 022 while the supervisor retains 077', async t => { const f=fixture(t);mkdirSync(f.out,{mode:0o700});const events=[],made=join(f.tempRoot,'step-file'); const original=process.umask(0o077); try { const result=await api.executeSteps([{name:'umask',cwd:f.checkout,argv:[process.execPath,'-e',`const fs=require('node:fs');console.log(process.umask().toString(8));fs.writeFileSync(${JSON.stringify(made)},'fixture',{mode:0o666});`]}],{out:f.out,env:process.env,record:r=>events.push(r)}); assert.equal(result.rc,0);assert.equal(process.umask(),0o077); assert.equal(fsExtra.readFileSync(join(f.out,'umask.log'),'utf8').trim(),'22'); assert.equal(fsExtra.statSync(made).mode&0o777,0o644); assert.equal(fsExtra.statSync(join(f.out,'umask.log')).mode&0o777,0o600); } finally {process.umask(original);} }); test('lane wrapper keeps its state directory and lock files private', t => { const f=fixture(t),script=new URL('../../../scripts/testing/gate-lane.sh',import.meta.url).pathname; const result=spawnSync('bash',[script,'recover','--state-root',f.stateRoot,'--run-id','000000000000','--grant','fixture-private-modes'],{encoding:'utf8'}); assert.equal(result.status,2,result.stdout+result.stderr);assert.match(result.stderr,/run not found/); assert.equal(fsExtra.statSync(f.stateRoot).mode&0o777,0o700); for(const name of ['a.lock','b.lock','image.lock'])assert.equal(fsExtra.statSync(join(f.stateRoot,name)).mode&0o777,0o600); });