// #650 strict reader negatives, real owned files only; prepared, not executed. import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { spawn, execFileSync } from 'node:child_process'; import { syncBuiltinESMExports } from 'node:module'; import { watchModelOutput } from '../src/model-stall.mjs'; const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms)); function scratch(t){const root=fs.mkdtempSync(join(tmpdir(),'reader650-'));t.after(()=>fs.rmSync(root,{recursive:true,force:true}));return root;} const canonical=()=>Buffer.from(`turn ${process.hrtime.bigint()}\n`); async function invalid(t,prepare){const root=scratch(t),file=join(root,'proxy-first-turn');prepare(file,root);let stops=0;const receipts=[];const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:60,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:r=>receipts.push(r)});t.after(watch.cancel);await delay(30);assert.equal(stops,1,'invalid first-turn metadata triggers single trusted stop');await delay(30);assert.equal(stops,1);assert.deepEqual(receipts,[{action:'proxy-stall',keyIds:['0123456789ab']}]);const targets=fs.readdirSync('/proc/self/fd').flatMap(n=>{try{return [fs.readlinkSync('/proc/self/fd/'+n)];}catch{return [];}});assert.equal(targets.includes(file),false,'invalid marker descriptor closed');return {root,file};} const malformed=[ ['oversized',()=>Buffer.alloc(65,120)],['empty',()=>Buffer.alloc(0)],['wrong prefix',()=>Buffer.from(`turns ${process.hrtime.bigint()}\n`)], ['zero',()=>Buffer.from('turn 0\n')],['future',()=>Buffer.from(`turn ${process.hrtime.bigint()+60_000_000_000n}\n`)],['leading zero',()=>Buffer.from(`turn 0${process.hrtime.bigint()}\n`)], ['extra trailing newline',()=>Buffer.concat([canonical(),Buffer.from('\n')])],['extra token',()=>Buffer.from(`turn ${process.hrtime.bigint()} extra\n`)], ['CRLF',()=>Buffer.from(`turn ${process.hrtime.bigint()}\r\n`)],['malformed digit',()=>Buffer.from('turn abc\n')], ...[['highbit prefix',0],['highbit digit',5],['highbit newline',-1]].map(([name,index])=>[name,()=>{const b=canonical();b[index<0?b.length-1:index]|=128;return b;}]), ]; for(const [name,bytes] of malformed)test(`650 reader rejects ${name} without exposing metadata`,async t=>{await invalid(t,file=>fs.writeFileSync(file,bytes(),{mode:0o600}));}); test('650 reader rejects wrong mode',async t=>{await invalid(t,file=>fs.writeFileSync(file,canonical(),{mode:0o644}));}); test('650 reader never follows owned symlink to a valid long-budget timestamp',async t=>{await invalid(t,(file,root)=>{const target=join(root,'owned-target');fs.writeFileSync(target,canonical(),{mode:0o600});fs.symlinkSync(target,file);});}); test('650 reader FIFO cannot block the watcher process',async t=>{ const root=scratch(t);execFileSync('mkfifo',['-m','600',join(root,'proxy-first-turn')]); const probe=new URL('./fixtures/model-clock-probe.mjs',import.meta.url).pathname; const child=spawn(process.execPath,[probe,root],{env:{PATH:process.env.PATH,HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,MOSAIC_CONFIG:process.env.MOSAIC_CONFIG},stdio:['ignore','pipe','ignore']}); let output='',timedOut=false;child.stdout.on('data',b=>{output+=b;});const closed=new Promise((resolve,reject)=>{child.once('error',reject);child.once('close',(code,signal)=>resolve({code,signal}));});const timer=setTimeout(()=>{timedOut=true;child.kill('SIGKILL');},5000);t.after(async()=>{clearTimeout(timer);if(child.exitCode===null&&child.signalCode===null)child.kill('SIGKILL');await closed;}); const result=await closed;clearTimeout(timer);assert.equal(timedOut,false,'FIFO probe completes without blocking');assert.equal(result.code,0);const answer=JSON.parse(output);assert.equal(answer.stops,1,'FIFO metadata is refused through existing stop');assert.equal(answer.nice,19);assert.equal(answer.openMarkerFds,0,'FIFO descriptor closed before child exit');assert.deepEqual(answer.receipts,[{action:'proxy-stall',keyIds:['0123456789ab']}]); }); test('650 reader late detection uses already-spent embedded budget',async t=>{const root=scratch(t);fs.writeFileSync(join(root,'proxy-first-turn'),`turn ${process.hrtime.bigint()-120_000_000_000n}\n`,{mode:0o600});let stops=0;const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:60,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:()=>{}});t.after(watch.cancel);await delay(30);assert.equal(stops,1,'embedded initial timestamp already spent the budget');}); test('650 reader closes owned marker descriptors and never rereads armed timestamp',async t=>{ const root=scratch(t),file=join(root,'proxy-first-turn');fs.writeFileSync(file,canonical(),{mode:0o600});const original=fs.openSync;let reads=0;fs.openSync=function(p,flags,...args){if(p===file&&typeof flags==='number')reads++;return original.call(this,p,flags,...args);};syncBuiltinESMExports();t.after(()=>{fs.openSync=original;syncBuiltinESMExports();}); const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:0.1,keyId:'0123456789ab',stop:async()=>{},receipt:()=>{}});t.after(watch.cancel);await delay(30);assert.equal(reads,1,'first marker read observed');fs.writeFileSync(file,canonical());await watch.done;assert.equal(reads,1,'armed timestamp never reread'); const targets=fs.readdirSync('/proc/self/fd').flatMap(n=>{try{return [fs.readlinkSync('/proc/self/fd/'+n)];}catch{return [];}});assert.equal(targets.includes(file),false,'all owned marker descriptors closed'); }); // Wrong-owner and independently unreadable valid-0600 fixtures require changing // another UID/ACL. Not forged here; those branches remain explicitly UNVERIFIED. test('650 first output cancels before malformed metadata can request a stop',async t=>{const root=scratch(t);fs.writeFileSync(join(root,'proxy-first-turn'),'not-a-timestamp',{mode:0o644});fs.writeFileSync(join(root,'proxy-first-output'),'output\n',{mode:0o600});let stops=0;const receipts=[];const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:60,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:r=>receipts.push(r)});t.after(watch.cancel);await watch.done;assert.equal(stops,0);assert.deepEqual(receipts,[]);});