import test from 'node:test';import assert from 'node:assert/strict'; import { mkdtempSync,rmSync,writeFileSync,mkdirSync,cpSync,readFileSync,statSync,symlinkSync,unlinkSync } from 'node:fs';import { join, isAbsolute } from 'node:path'; import { spawnSync } from 'node:child_process'; const image=await import('../../../scripts/testing/lane-image.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};}); const docker=await import('../../../scripts/testing/lane-docker.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};}); function fixtureBase(root){return /^FROM (\S+)$/m.exec(readFileSync(join(root,'Containerfile'),'utf8'))[1];} const processingInputs=['packages/processing-control/src/state.mjs','packages/processing-control/src/entrypoint.mjs']; function copyProcessingInputs(root){ mkdirSync(join(root,'packages/processing-control/src'),{recursive:true}); for(const file of processingInputs)cpSync(new URL('../../../'+file,import.meta.url),join(root,file)); } test('a changed COPY input or actual image ID refuses compatible image sharing',t=>{ assert.equal(typeof image.imageFingerprint,'function','image fingerprint required'); const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true})); for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f)); copyProcessingInputs(root); writeFileSync(join(root,'RELEASE'),'0.0.1\n'); for(const d of ['contracts','src','adapters']){mkdirSync(join(root,d));writeFileSync(join(root,d,'one'),'same');} const digest=image.imageFingerprint(root),id='sha256:'+'a'.repeat(64);const receipt={inputDigest:digest,imageId:id,tag:'mosaic-poc-agent:0.85.1-r0.0.1',baseDigest:fixtureBase(root),platform:'linux/amd64'}; const inspect=()=>({Id:id,Os:'linux',Architecture:'amd64'}); assert.equal(image.verifyImage(receipt,root,inspect).imageId,id); assert.throws(()=>image.verifyImage(receipt,root,()=>({...inspect(),Id:'sha256:'+'c'.repeat(64)})),/image ID/); writeFileSync(join(root,'adapters','one'),'different');assert.throws(()=>image.verifyImage(receipt,root,inspect),/inputs/); }); test('warm Docker guard disables implicit build and pull and refuses image mutation',t=>{ assert.equal(typeof docker.guardDocker,'function','Docker guard required'); const run=composeFixture(t); assert.deepEqual(docker.guardDocker(['compose','run','--rm','-T','mosaic-agent','prompt'],run,run.env),['compose','-f',join(run.checkout,'compose.yaml'),'-f',join(run.tempRoot,'compose-no-build.yaml'),'run','--pull','never','--no-deps','--rm','-T','mosaic-agent','prompt']); for(const args of [['build','.'],['image','prune'],['compose','build'],['tag','a','b'],['network','rm','foreign']])assert.throws(()=>docker.guardDocker(args,run),/refused/); }); test('exclusive image preparation records actual image ID and build wall time and requires a grant',t=>{ assert.equal(typeof image.prepareImage,'function','prepareImage is required'); const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true})); for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f)); copyProcessingInputs(root); writeFileSync(join(root,'RELEASE'),'0.0.1\n');for(const d of ['contracts','src','adapters'])mkdirSync(join(root,d)); const calls=[];const invoke=(args)=>{calls.push(args);if(args[0]==='compose')return '';return JSON.stringify({Id:'sha256:'+'a'.repeat(64),Os:'linux',Architecture:'amd64'});}; assert.throws(()=>image.prepareImage(root,{grant:'',tempRoot:root},invoke),/grant/); const result=image.prepareImage(root,{grant:'fixture',tempRoot:root},invoke);assert.equal(result.imageId,'sha256:'+'a'.repeat(64));assert.equal(result.builds,1);assert.ok(result.buildWallMs>=0);assert.ok(calls.some(x=>x.join(' ')==='compose build')); }); test('cleanup refuses a foreign Docker label and removes only an exact owned project',()=>{ const project='mg197-a-abcdef123456',id='123456789abc';let removed=false,label='foreign'; const fake=(args)=>{if(args[0]==='ps')return removed?'':id;if(args[0]==='network')return '';if(args[0]==='container'&&args[1]==='inspect')return label;if(args[0]==='container'&&args[1]==='rm'){removed=true;return '';}throw Error('unexpected command');}; // The injectable executor exercises ownership checks without touching Docker. assert.throws(()=>docker.cleanupProject({project},fake),/foreign/);assert.equal(removed,false); label=project;assert.deepEqual(docker.cleanupProject({project},fake).removed,[{kind:'container',id}]);assert.equal(removed,true); }); function imageFixture(t){ const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true})); for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f)); copyProcessingInputs(root); writeFileSync(join(root,'RELEASE'),'0.0.1\n');for(const d of ['contracts','src','adapters'])mkdirSync(join(root,d)); const receipt={inputDigest:image.imageFingerprint(root),imageId:'sha256:'+'a'.repeat(64),tag:'mosaic-poc-agent:0.85.1-r0.0.1',baseDigest:fixtureBase(root),platform:'linux/amd64'}; return {root,receipt}; } test('missing receipt and missing image refuse without permitting a gate',t=>{ const {root,receipt}=imageFixture(t);assert.throws(()=>image.verifyImage(null,root),/provenance receipt/); assert.throws(()=>image.verifyImage(receipt,root,()=>{throw Error('No such image');}),/No such image/); assert.throws(()=>image.verifyImage({...receipt,tag:'mosaic-poc-agent:wrong'},root,()=>({Id:receipt.imageId,Os:'linux',Architecture:'amd64'})),/release identity/); }); test('a tag changed between supervised steps fails the next boundary and suppresses gate success',async t=>{ const {root,receipt}=imageFixture(t),out=join(root,'out');mkdirSync(out); let actual=receipt.imageId;const inspect=()=>({Id:actual,Os:'linux',Architecture:'amd64'}),events=[]; const {executePinnedSteps}=await import('../../../scripts/testing/gate-lane.mjs'); await assert.rejects(executePinnedSteps([ {name:'first',cwd:root,argv:[process.execPath,'-e','process.exit(0)']}, {name:'second',cwd:root,argv:[process.execPath,'-e','process.exit(0)']} ],{out,env:process.env,record:r=>events.push(r)}, {receipt,checkout:root,inspect,afterStep:()=>actual='sha256:'+'c'.repeat(64)}),/image ID/); assert.equal(events.filter(e=>e.kind==='step').length,1); assert.equal(events.some(e=>e.kind==='terminal'&&e.rc===0),false); }); test('pinned-base rebuilds with identical files have distinct image IDs; cold and warm counters stay separate',t=>{ const {root}=imageFixture(t);let builds=0,id='sha256:'+'a'.repeat(64); const fake=args=>{if(args[0]==='compose'){builds++;return 'fixture build';}return JSON.stringify({Id:id,Os:'linux',Architecture:'amd64'});}; const cold=image.prepareImage(root,{grant:'fixture-cold',tempRoot:root},fake); assert.equal(cold.builds,1);assert.ok(cold.buildWallMs>=0);assert.equal(builds,1); for(let i=0;i<3;i++)image.verifyImage(cold,root,()=>({Id:id,Os:'linux',Architecture:'amd64'})); assert.equal(builds,1,'warm verification builds zero images'); id='sha256:'+'c'.repeat(64);const rebuilt=image.prepareImage(root,{grant:'fixture-rebuild',tempRoot:root},fake); assert.equal(rebuilt.inputDigest,cold.inputDigest);assert.equal(rebuilt.builds,1);assert.equal(builds,2); assert.throws(()=>image.verifyImage(cold,root,()=>({Id:rebuilt.imageId,Os:'linux',Architecture:'amd64'})),/image ID/); }); test('warm pinned execution records zero builds and checks the tag again after its final step',async t=>{ const {root,receipt}=imageFixture(t),out=join(root,'out');mkdirSync(out);let id=receipt.imageId; const {executePinnedSteps}=await import('../../../scripts/testing/gate-lane.mjs'); const options={out,env:process.env,record:()=>{}},inspect=()=>({Id:id,Os:'linux',Architecture:'amd64'}); const result=await executePinnedSteps([{name:'warm',cwd:root,argv:[process.execPath,'-e','process.exit(0)']}],options,{receipt,checkout:root,inspect}); assert.equal(result.rc,0);assert.equal(result.imageBuilds,0); await assert.rejects(executePinnedSteps([{name:'last',cwd:root,argv:[process.execPath,'-e','process.exit(0)']}],options,{receipt,checkout:root,inspect,afterStep:()=>id='sha256:'+'c'.repeat(64)}),/image ID/); }); // These two read-only compatibility probes must reach the installed binary, // including when the lane's guard is first on PATH. Child fixture env stays // isolated; passing the real absolute path avoids re-entering that guard. function installedDocker(){ const file=process.env.MOSAIC_GATE_DOCKER_RECORD; if(!file)return 'docker'; const binary=JSON.parse(readFileSync(file,'utf8')).docker; assert.ok(typeof binary==='string'&&isAbsolute(binary),'lane record must name the real absolute Docker binary'); return binary; } function composeFixture(t) { const tempRoot=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(tempRoot,{recursive:true,force:true})); const checkout=join(tempRoot,'candidate');mkdirSync(checkout);cpSync(new URL('../../../compose.yaml',import.meta.url),join(checkout,'compose.yaml')); const config=join(tempRoot,'docker-config');mkdirSync(config,{mode:0o700}); const auth=join(tempRoot,'fixture-auth.json');writeFileSync(auth,'{}',{mode:0o600}); return {tempRoot,checkout,project:'mg197-a-abcdef123456',tag:'mosaic-poc-agent:fixture',imageId:'sha256:'+'a'.repeat(64), env:{PATH:process.env.PATH,HOME:process.env.HOME,DOCKER_CONFIG:config,COMPOSE_PROJECT_NAME:'mg197-a-abcdef123456', MOSAIC_IMAGE_TAG:'mosaic-poc-agent:fixture',MOSAIC_PROVIDER:'fixture',MOSAIC_MODEL:'fixture',MOSAIC_DATA_ROOT:tempRoot,PI_AUTH_FILE:auth}}; } test('installed Compose accepts rewritten run argv without starting a container',t=>{ const run=composeFixture(t); if(docker.writeComposeOverride)docker.writeComposeOverride(run); const argv=docker.guardDocker(['compose','run','--rm','-T','mosaic-agent','fixture'],run,run.env); const command=argv.indexOf('run');assert.ok(command>0); // --help is before SERVICE, so parsing exits before any service operation. const parsed=spawnSync(installedDocker(),[...argv.slice(0,command+1),'--help',...argv.slice(command+1)],{cwd:run.checkout,env:run.env,encoding:'utf8',timeout:10000}); assert.equal(parsed.status,0,parsed.stdout+parsed.stderr); assert.match(parsed.stdout,/Usage:/); }); test('installed Compose resolves the private override with no build and the pinned image',t=>{ const run=composeFixture(t);assert.equal(typeof docker.writeComposeOverride,'function'); const override=docker.writeComposeOverride(run); assert.equal(override,join(run.tempRoot,'compose-no-build.yaml')); assert.equal(statSync(override).mode&0o777,0o600); assert.equal(docker.writeComposeOverride(run),override,'parallel callers reuse identical private content'); const argv=docker.guardDocker(['compose','run','--rm','mosaic-agent'],run,run.env),command=argv.indexOf('run'); const resolved=spawnSync(installedDocker(),[...argv.slice(0,command),'config','--format','json'],{cwd:run.checkout,env:run.env,encoding:'utf8',timeout:10000}); assert.equal(resolved.status,0,resolved.stdout+resolved.stderr); const service=JSON.parse(resolved.stdout).services['mosaic-agent']; assert.equal(Object.hasOwn(service,'build'),false);assert.equal(service.image,run.imageId); t.diagnostic(JSON.stringify({installedComposeConfig:{buildPresent:Object.hasOwn(service,'build'),image:service.image}})); assert.ok(argv.includes('--no-deps'));assert.equal(argv[argv.indexOf('--pull')+1],'never'); writeFileSync(override,'changed fixture');assert.throws(()=>docker.writeComposeOverride(run),/override/); unlinkSync(override);const foreign=join(run.tempRoot,'foreign');writeFileSync(foreign,'fixture');symlinkSync(foreign,override); assert.throws(()=>docker.writeComposeOverride(run),/override/);assert.equal(readFileSync(foreign,'utf8'),'fixture'); }); test('guard owns every Compose file argument and refuses caller configuration overrides',t=>{ const run=composeFixture(t); for(const args of [['-f','foreign.yaml'],['--file','foreign.yaml'],['--file=foreign.yaml'],['--project-directory','/tmp'],['--build'],['--pull','always'],['--no-build']]) assert.throws(()=>docker.guardDocker(['compose','run',...args,'mosaic-agent'],run),/options not allowed/); for(const args of [['compose','-f','foreign.yaml','run','mosaic-agent'],['compose','--project-directory','/tmp','run','mosaic-agent']]) assert.throws(()=>docker.guardDocker(args,run),/unowned command/); }); // Row 191 image-input manifest: either copied reader must invalidate the pinned image. for(const file of processingInputs)test(`changing ${file} invalidates the image receipt`,t=>{ const {root,receipt}=imageFixture(t); const inspect=()=>({Id:receipt.imageId,Os:'linux',Architecture:'amd64'}); assert.equal(image.verifyImage(receipt,root,inspect).imageId,receipt.imageId); writeFileSync(join(root,file),readFileSync(join(root,file),'utf8')+'\n// changed fixture input\n'); assert.throws(()=>image.verifyImage(receipt,root,inspect),/image inputs differ/); });