// The stack-release stand-ins and module fixture, shared by stack-release.test.mjs and // stack-release-external.test.mjs (row 232). Moved unchanged from stack-release.test.mjs, plus // what the external tests need; each test file runs in its own process, so each gets its own base. import test, {after} from 'node:test'; import assert from 'node:assert/strict'; import {execFileSync,spawn,spawnSync} from 'node:child_process'; import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,appendFileSync,cpSync,rmSync,chmodSync,existsSync,statSync,lstatSync,symlinkSync,linkSync,readdirSync,realpathSync} from 'node:fs'; import {createServer} from 'node:http'; import {createHash} from 'node:crypto'; import {DatabaseSync} from 'node:sqlite'; import {tmpdir} from 'node:os'; import {join,dirname,basename} from 'node:path'; import {fileURLToPath,pathToFileURL} from 'node:url'; import {once} from 'node:events'; import {organizationImportedFiles} from './organization-imports.mjs'; export const ROOT=fileURLToPath(new URL('../../..',import.meta.url)).replace(/\/$/,''); export const NODE=process.execPath; export const MARKER='# mosaic-cli wrapper (Q81, agents/sage/work/q81/mosaic-cli.sh)'; export const BUS='mosaic-bus@acme.service',CON='mosaic-webui.service'; export const git=(cwd,...args)=>execFileSync('git',['-c','user.name=fixture','-c','user.email=fixture@invalid','-c','commit.gpgsign=false','-c','core.hooksPath=/dev/null','-C',cwd,...args],{encoding:'utf8'}).trim(); export const script=(fn)=>`#!${NODE}\n(${fn})();\n`; // --- Stand-ins. Each is serialized into its own file, so it uses only process and its own scope. --- // systemctl --user: unit state in $SD_STATE/units.json, every call in calls.log. A bus start runs // holder.cjs (the store held open in WAL mode, writer.lock, host.json) and, with --pm in the unit's // ExecStart, writes the `launched` journal line and the launches entry unless a pm- fault // names another case. Faults are files in $SD_STATE/faults named for the copy they hit. Units of a // named instance (row 190) run on their own data root and host files beside the default's. export function systemctl(){ const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path'),cp=process.getBuiltinModule('node:child_process'),crypto=process.getBuiltinModule('node:crypto'); const sd=process.env.SD_STATE,data=process.env.SD_DATA,unitDir=path.join(process.env.XDG_CONFIG_HOME,'systemd','user'); const args=process.argv.slice(2).filter((a)=>a!=='--user'),[verb,...rest]=args; fs.appendFileSync(path.join(sd,'calls.log'),args.join(' ')+'\n'); // Row 232: argv.log keeps each call as given, --user and all, for the user/external argv tests. fs.appendFileSync(path.join(sd,'argv.log'),process.argv.slice(2).join(' ')+'\n'); const faultFile=(n)=>path.join(sd,'faults',n),fault=(n)=>fs.existsSync(faultFile(n)); const stFile=path.join(sd,'units.json'); const load=()=>{try{return JSON.parse(fs.readFileSync(stFile,'utf8'))}catch{return {}}}; const save=(s)=>{fs.writeFileSync(stFile+'.tmp',JSON.stringify(s));fs.renameSync(stFile+'.tmp',stFile)}; const sleep=(ms)=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms); const alive=(pid)=>{try{const s=fs.readFileSync(`/proc/${pid}/stat`,'utf8');return s[s.lastIndexOf(')')+2]!=='Z'}catch{return false}}; // Row 190: a bus unit is the default's mosaic-bus@B or instance NAME's mosaic-bus-NAME@B; %i is B. const isBus=(u)=>/^mosaic-bus(-[a-z][a-z0-9]{1,15})?@[a-z0-9-]+\.service$/.test(u),inst=(u)=>u.slice(u.indexOf('@')+1,-'.service'.length); const unitFile=(u)=>{ const main=isBus(u)?u.replace(/@[^@]*$/,'@.service'):u,file=path.join(unitDir,main); if(!fs.existsSync(file))return null; // The drop-in directories in systemd's order: service.d, the name cut after each hyphen, then // the template's and the unit's own. const stem=main.slice(0,main.search(/@?\.service$/)),dirs=['service.d'],drops=[]; for(let i=0;i{const c=f&&f.env.find((v)=>v.startsWith('MOSAIC_CONFIG='));return c?JSON.parse(fs.readFileSync(c.slice('MOSAIC_CONFIG='.length),'utf8')).dataRoot:data}; const perData=(d,n)=>path.join(sd,d===data?`${n}.json`:`${n}-${path.basename(d)}.json`); const snap=(ev,d)=>{ const dir=path.join(d,'bus'),files={}; try{for(const n of fs.readdirSync(dir).sort()){const p=path.join(dir,n),st=fs.statSync(p);if(st.isFile())files[n]=[st.size,st.mtimeMs,crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex')]}}catch{} fs.appendFileSync(path.join(sd,'snap.jsonl'),JSON.stringify({ev,files})+'\n'); }; const pm=(unit,d,id,pid,off)=>{ let mode='';try{mode=fs.readFileSync(faultFile(`pm-${id}`),'utf8').trim()}catch{} const b=inst(unit),now=Date.now()-off*1000,epoch=Math.floor(now/1000),at=new Date(now).toISOString(),run=`run-${pid}`; const line=(text)=>fs.appendFileSync(path.join(sd,'journal.jsonl'),JSON.stringify({epoch,unit,text})+'\n'); const ok=()=>line(`launched pm-${b}: run ${run}, claude opus`); const lf=perData(d,'launches');let list=[];try{list=JSON.parse(fs.readFileSync(lf,'utf8'))}catch{} // instance is launch.by's, chief in the example business, as the external PM check reads (row 232). const entry=(o)=>list.push({run,instance:'chief',business:b,live:true,startedAt:at,...o}); line(`bus host up: business ${b}, socket /fixture/bus.sock, notifier off`); switch(mode){ case 'none':break; case 'two':ok();line(`launched pm-${b}: run ${run}b, claude opus`);entry({});break; case 'badword':line(`launched pm-${b}: job ${run}, claude opus`);entry({});break; case 'notlisted':ok();break; case 'early':ok();entry({startedAt:new Date(now-3600e3).toISOString()});break; case 'human':entry({run:`human-${pid}`});break; case 'other':ok();entry({business:'other'});break; case 'dead':ok();entry({live:false});break; // Row 232: two live entries for the PM's instance, and one for it beside a live coder run. case 'twolive':ok();entry({});entry({run:`${run}b`});break; case 'coder':ok();entry({});entry({run:`${run}c`,instance:'coder'});break; default:ok();entry({}); } fs.writeFileSync(lf,JSON.stringify(list)); }; if(verb==='show'){ // `show [--timestamp=unix] UNIT -p P...` and `show -p P UNIT`, as the external path calls it. let u='';const props=[];for(let i=0;i{ if(!fault('enter'))return st.enter; const o=JSON.parse(fs.readFileSync(faultFile('enter'),'utf8'));return o.at!==undefined&&Date.now()>=o.at?o.then:o.value; }; for(const p of props){ if(p==='LoadState')console.log(`LoadState=${f?'loaded':'not-found'}`); else if(p==='WorkingDirectory')console.log(`WorkingDirectory=${f?f.wd+(!isBus(u)&&fault(`check-${id}`)?'-moved':''):''}`); else if(p==='ExecStart')console.log(f&&f.exec?`ExecStart={ path=${f.exec.split(' ')[0]} ; argv[]=${f.exec} ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }`:'ExecStart='); else if(p==='Environment')console.log(`Environment=${f?f.env.join(' '):''}`); else if(p==='MainPID')console.log(`MainPID=${st.pid||0}`); else if(p==='DropInPaths')console.log(`DropInPaths=${f?f.drops.join(' '):''}`); else if(p==='FragmentPath')console.log(`FragmentPath=${f&&!fault('no-fragment')?f.file:''}`); else if(p==='ActiveEnterTimestamp'){const v=st.state==='active'?enter():undefined;console.log(`ActiveEnterTimestamp=${v===undefined?'':'@'+v}`)} } process.exit(0); } if(verb==='is-active'){ const q=rest[0]==='--quiet',u=q?rest[1]:rest[0];let s=(load()[u]||{}).state||'inactive'; // bus-stops-after N (T29): the next N bus checks answer the real state, every later one answers // inactive, and units.json stays as it is, as if the bus stopped between two checks. if(isBus(u)&&fault('bus-stops-after')){const n=Number(fs.readFileSync(faultFile('bus-stops-after'),'utf8'));if(n>0)fs.writeFileSync(faultFile('bus-stops-after'),String(n-1));else s='inactive'} if(!q)console.log(s);process.exit(s==='active'?0:3); } if(verb==='is-enabled'){console.log('disabled');process.exit(1)} if(verb==='daemon-reload')process.exit(0); if(verb==='list-units'){ if(fault('list-fails'))process.exit(1); const pats=rest.filter((a)=>!a.startsWith('--')).map((p)=>new RegExp('^'+p.replace(/\./g,'\\.').replace(/\*/g,'.*')+'$')); for(const [u,s] of Object.entries(load()))if(pats.some((re)=>re.test(u)))console.log(`${u} loaded ${s.state} ${s.state==='active'?'running':'dead'} fixture`); process.exit(0); } if(verb==='start'){ const u=rest[0],f=unitFile(u);if(!f){console.error(`Unit ${u} not found.`);process.exit(5)} const s=load(),id=path.basename(f.wd),d=dataOf(f); const enter=Math.floor((Date.now()-Number(process.env.SD_CLOCK_OFFSET||'0')*1000)/1000); if(!isBus(u)){s[u]={state:'active',pid:null,wd:f.wd,enter};save(s);process.exit(0)} if(fault(`start-dies-${id}`)){s[u]={state:'failed',pid:null,wd:f.wd};save(s);process.exit(0)} // Row 185 (T48): the copy's start writes one event before the holder opens the store, so the // store differs from a backup the run took before the start. The close leaves no -wal. if(fault(`start-writes-row-${id}`)){ const db=new (process.getBuiltinModule('node:sqlite').DatabaseSync)(path.join(d,'bus','bus.sqlite')); db.prepare('INSERT INTO events (at) VALUES (?)').run('2999-01-01T00:00:00.000Z');db.close(); } snap('start',d); const off=process.env.SD_CLOCK_OFFSET||'0'; // The holder must not inherit stack-release.sh's lock (fd 9) or any other fd above 2. const close='for fd in /proc/self/fd/*; do n=${fd##*/}; [ "$n" -gt 2 ] && eval "exec $n>&-" 2>/dev/null; done; exec "$@"'; const ch=cp.spawn('bash',['-c',close,'holder',process.execPath,path.join(__dirname,'..','holder.cjs'),d,inst(u),sd,off,perData(d,'host')],{detached:true,stdio:'ignore'}); ch.unref(); fs.appendFileSync(path.join(sd,'pids.log'),ch.pid+'\n'); const ready=path.join(sd,`ready-${ch.pid}`),end=Date.now()+10000; while(!fs.existsSync(ready)){if(Date.now()>end){console.error('fixture holder did not start');process.exit(1)}sleep(20)} s[u]={state:'active',pid:ch.pid,wd:f.wd,enter};save(s); if(f.exec.split(' ').slice(1).includes('--pm'))pm(u,d,id,ch.pid,Number(off)); process.exit(0); } if(verb==='stop'){ const u=rest[0],s=load(),st=s[u]; if(st&&st.pid){ try{process.kill(st.pid,'SIGTERM')}catch{} const end=Date.now()+10000;while(alive(st.pid)&&Date.now()JSON.parse(l))}catch{} for(const o of lines)if(o.unit===unit&&o.epoch>=from)console.log(o.text); } export function systemdAnalyze(){ const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path'); if(process.argv[2]!=='--user'||process.argv[3]!=='unit-paths')process.exit(1); console.log(path.join(process.env.XDG_CONFIG_HOME,'systemd','user')); // The unit-path fault adds its text as one more path, as T28's relative path needs. const extra=path.join(process.env.SD_STATE,'faults','unit-path'); if(fs.existsSync(extra))console.log(fs.readFileSync(extra,'utf8')); } // The bus host: holds the store open in WAL mode until SIGTERM, as the broker does. It writes its // status to the host file systemctl names for its data root. export function holder(){ const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path'),{DatabaseSync}=process.getBuiltinModule('node:sqlite'); const [data,business,sd,offset,host]=process.argv.slice(2),bus=path.join(data,'bus'); fs.mkdirSync(bus,{recursive:true,mode:0o700}); const db=new DatabaseSync(path.join(bus,'bus.sqlite')); if(!db.prepare("SELECT count(*) AS n FROM sqlite_master WHERE name='meta'").get().n){ db.exec('PRAGMA journal_mode=WAL'); db.exec('CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)'); db.exec('CREATE TABLE events (id INTEGER PRIMARY KEY, at TEXT NOT NULL)'); db.prepare("INSERT INTO meta VALUES ('schema_version', '3g')").run(); } fs.writeFileSync(path.join(bus,'writer.lock'),String(process.pid)); const at=new Date(Date.now()-Number(offset||0)*1000).toISOString(); fs.writeFileSync(host+'.tmp',JSON.stringify({host:{business,pid:process.pid,live:true,startedAt:at,notifier:'ok'},boot:{ok:true,business,at}})); fs.renameSync(host+'.tmp',host); process.on('SIGTERM',()=>{db.close();fs.rmSync(path.join(bus,'writer.lock'),{force:true});process.exit(0)}); fs.writeFileSync(path.join(sd,`ready-${process.pid}`),''); setInterval(()=>{},1<<30); } // A copy's scripts/mosaic: bus status and launches list from what the holder and systemctl wrote // for the data root of the config it runs under (row 190), the default's being $SD_DATA. export function mosaicStub(){ const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path'); const id=path.basename(path.resolve(__dirname,'..')),sd=process.env.SD_STATE,a=process.argv.slice(2).join(' '); const fault=(n)=>fs.existsSync(path.join(sd,'faults',n)); let d=process.env.SD_DATA;try{d=JSON.parse(fs.readFileSync(process.env.MOSAIC_CONFIG,'utf8')).dataRoot}catch{} const perData=(n)=>path.join(sd,d===process.env.SD_DATA?`${n}.json`:`${n}-${path.basename(d)}.json`); const alive=(pid)=>{try{const s=fs.readFileSync(`/proc/${pid}/stat`,'utf8');return s[s.lastIndexOf(')')+2]!=='Z'}catch{return false}}; if(a==='bus status --json'){ if(fault(`status-${id}`))process.exit(1); // Row 232: status-until- fails bus status until the epoch ms it holds. const until=path.join(sd,'faults',`status-until-${id}`); if(fs.existsSync(until)&&Date.now()({name,sums:fs.existsSync(path.join(dir,name,'SHA256SUMS'))})):[]; fs.writeFileSync(path.join(sd,'migrate.json'),JSON.stringify({backups})); const fail=fs.existsSync(path.join(sd,'faults','migrate-fails')); const db=new DatabaseSync(path.join(data,'bus','bus.sqlite')); db.prepare("UPDATE meta SET value=? WHERE key='schema_version'").run(fail?'3h-partial':'3h');db.close(); if(fail)process.exit(1); console.log('fixture migrate: 3h'); } // --- The module fixture: one repo, its copies and the stand-ins, shared by every test. --- export const base=realpathSync(mkdtempSync(join(tmpdir(),'stack-release-'))); after(()=>{execFileSync('chmod',['-R','u+w',base]);rmSync(base,{recursive:true,force:true})}); export const repo=join(base,'repo'),COPIES=join(base,'copies'),SBIN=join(base,'sbin'); mkdirSync(repo);mkdirSync(COPIES);mkdirSync(SBIN); writeFileSync(join(base,'package.json'),'{"type":"commonjs"}\n'); // A linked node_modules (a deploy tree) is not a directory to .gitignore's node_modules/; npm ci makes the copy's own. export const tracked=execFileSync('git',['-C',ROOT,'ls-files','-co','--exclude-standard','--','scripts','packages','adapters','roles','RELEASE','package.json','package-lock.json',':!packages/*/tests',':!scripts/foundation',':(exclude,glob)**/node_modules'],{encoding:'utf8'}).trim().split('\n'); for(const f of tracked)if(existsSync(join(ROOT,f)))cpSync(join(ROOT,f),join(repo,f)); export const pkg={name:'copy-fixture',version:'1.0.0'}; writeFileSync(join(repo,'package.json'),JSON.stringify(pkg)); writeFileSync(join(repo,'package-lock.json'),JSON.stringify({name:pkg.name,version:pkg.version,lockfileVersion:3,packages:{'':pkg}})); git(repo,'init','-q','-b','refactor');git(repo,'config','gc.auto','0');git(repo,'add','.');git(repo,'commit','-qm','fixture'); export const commit=(msg)=>{git(repo,'commit','-q','--allow-empty','-m',msg);return git(repo,'rev-parse','HEAD')}; // refactor: F A B C1 C, then M merging side branch S (from A). U sits on a branch nothing reaches. export const F=git(repo,'rev-parse','HEAD'),A=commit('row 901: A'),B=commit('row 902: B'),C1=commit('row 903: C1'),C=commit('row 904: C'); git(repo,'checkout','-q','-b','side',A);export const S=commit('row 905: S'); git(repo,'checkout','-q','-b','unreached',A);export const U=commit('row 906: U'); git(repo,'checkout','-q','refactor');git(repo,'merge','-q','--no-ff','-m','merge side','side'); export const M=git(repo,'rev-parse','HEAD');git(repo,'update-ref','refs/remotes/origin/refactor',M); // `to` false leaves out the `to:` lines (C1); a string puts that value in each one (A). The --plan // steps line can't be read from either (T27). export const storeText=(v,froms,to=true)=>`export const SCHEMA_VERSION = '${v}';\nexport const MIGRATIONS = [\n${froms.map((f,i)=>` {\n from: '${f}',\n${to===false?'':` to: '${to===true?froms[i+1]??v:to}',\n`} run() {},\n },\n`).join('')}];\n`; // Plug-in kinds (row 169): C1 and C know the harness kind, as a copy at or past c216f47db does. The // rest have it under another name, as ca15d73e9 has no harness kind. Every copy has the real business // loader (packages/organization/src, and the packages/business/src shims the tool imports, rename r2 // stage B) and roles, which the tool reads the business file with. export const KINDS_NEW=readFileSync(join(repo,'packages','plugins','src','kinds.mjs'),'utf8'); export const KINDS_OLD=KINDS_NEW.replace('\n harness: Object.freeze({','\n "harness-before-c216f47db": Object.freeze({'); assert.notEqual(KINDS_OLD,KINDS_NEW); // Row 200 (#1698): C1 and C take --sso and have the row 192 loader and checkConsole; C's checker // refuses RS256, a stand-in for a target whose rules differ from the repository's. B has the loader // from before row 192, which refuses a `console` key. The other copies have no loader. Row 203: B's // loader also refuses a `federation` key, as a pre-203 loader does (its list has no federation). const LOADER=readFileSync(join(repo,'scripts','mosaic-config.mjs'),'utf8'),SSO_CONFIG=readFileSync(join(repo,'packages','webui','src','sso-config.mjs'),'utf8'); const LOADER_OLD=LOADER.replace('["configVersion", "environment", "dataRoot", "execution", "console", "federation"]','["configVersion", "environment", "dataRoot", "execution"]'); const SSO_CONFIG_C=SSO_CONFIG.replace("'ES256', 'RS256',","'ES256',"); assert.notEqual(LOADER_OLD,LOADER);assert.notEqual(SSO_CONFIG_C,SSO_CONFIG); const ssoFiles=(id)=>id===B?[['scripts/mosaic-config.mjs',LOADER_OLD]]:[C1,C].includes(id)?[['scripts/mosaic-config.mjs',LOADER],['packages/webui/src/sso-config.mjs',id===C?SSO_CONFIG_C:SSO_CONFIG]]:[]; for(const [id,store] of [[F,'// a copy from before SCHEMA_VERSION\n'],[A,storeText('3g',['3f'],'3x')],[B,storeText('3g',['3f'])],[C1,storeText('3g',['3f'],false)],[C,storeText('3h',['3f','3g'])],[S,storeText('3g',['3f'])],[U,storeText('3g',['3f'])]]){ const d=join(COPIES,id); for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n${[C1,C].includes(id)?"// serve accepts '--sso' (row 192)\n":''}`:'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){ mkdirSync(dirname(join(d,f)),{recursive:true});writeFileSync(join(d,f),text,{mode}); } for(const p of ['packages/organization/src','packages/business/src','packages/library/src','roles'])cpSync(join(repo,p),join(d,p),{recursive:true}); // Row 333 R2 (#1831): what the business loader imports from outside its directory (kinds.mjs, which is // already here in its OLD or NEW form, and model-quota.mjs). for(const p of organizationImportedFiles(repo))if(!existsSync(join(d,p)))cpSync(join(repo,p),join(d,p)); execFileSync('chmod',['-R','a-w',d]); } for(const [name,fn] of [['systemctl',systemctl],['journalctl',journalctl],['systemd-analyze',systemdAnalyze]])writeFileSync(join(SBIN,name),script(fn),{mode:0o755}); writeFileSync(join(base,'holder.cjs'),`(${holder})();\n`); export const SR=join(repo,'scripts','stack-release.sh'); // --- Per-test roots and helpers --- export const sha=(f)=>createHash('sha256').update(readFileSync(f)).digest('hex'); export const lines=(f)=>existsSync(f)?readFileSync(f,'utf8').split('\n').filter(Boolean):[]; export const calls=(e)=>lines(join(e.sd,'calls.log')); export const startsStops=(e,from)=>calls(e).slice(from).filter((c)=>/^(start|stop) /.test(c)); export const logLines=(e)=>lines(e.log).map((l)=>JSON.parse(l)); export const pointer=(e)=>existsSync(e.pointer)?JSON.parse(readFileSync(e.pointer,'utf8')):null; export const units=(e)=>JSON.parse(readFileSync(join(e.sd,'units.json'),'utf8')); export const snaps=(e)=>lines(join(e.sd,'snap.jsonl')).map((l)=>JSON.parse(l)); export const fault=(e,name,text='')=>writeFileSync(join(e.sd,'faults',name),text); export const sysctl=(e,...a)=>execFileSync(join(SBIN,'systemctl'),['--user',...a],{env:e.env,encoding:'utf8'}); export const edit=(f,from,to)=>{const s=readFileSync(f,'utf8');assert.ok(s.includes(from),`${f} has no ${from}`);writeFileSync(f,s.replace(from,to))}; export const posts=(e)=>readdirSync(e.data).filter((n)=>n.startsWith('bus.post-attempt-')); export const backupSum=(e,act)=>readFileSync(join(e.data,'backups','stack',act,'SHA256SUMS'),'utf8').split(' ')[0]; export const commitLines=(range)=>git(repo,'log','--oneline','--no-merges','--max-count=200',range).split('\n').map((l)=>` ${l}`); // The units, the wrapper, the pointer and the store, by sha256. export const fp=(e)=>({bus:sha(join(e.units,e.tmpl)),console:sha(join(e.units,e.con)),wrapper:sha(join(e.bin,e.wrapper)),pointer:existsSync(e.pointer)?sha(e.pointer):null,store:sha(e.store)}); // The schema version: readOnly while the holder has the store open, immutable when it is stopped. export function storeVersion(file,held){ const db=held?new DatabaseSync(file,{readOnly:true}):new DatabaseSync(pathToFileURL(file).href+'?immutable=1',{readOnly:true}); try{return db.prepare("SELECT value FROM meta WHERE key='schema_version'").get().value}finally{db.close()} } // Every file and directory under p with size and mtime: T23 and T16 compare it before and after. export function tree(p){ const out=[]; const walk=(q)=>{let st;try{st=statSync(q)}catch{return}out.push(st.isDirectory()?`${q}/`:`${q} ${st.size} ${st.mtimeMs}`);if(st.isDirectory())for(const n of readdirSync(q).sort())walk(join(q,n))}; walk(p);return out; } // T17: what a run may never remove. Under --units external confirm removes a set's AWAITING-UNITS // and FAILED when it closes the activation (row 232, r6 A5 and R4); user mode writes neither. const markers=/\/(AWAITING-UNITS|FAILED) /; export const kept=(e)=>[...tree(join(e.data,'backups','stack')),...tree(join(e.state,'stack-rollback')).filter((l)=>!markers.test(l)),...posts(e).flatMap((n)=>tree(join(e.data,n))),...tree(COPIES)].map((l)=>l.replace(/ .*/,'')); // The business file (row 169): the committed example made loadable, as acme, with `plugins` when given. export const EXAMPLE=readFileSync(join(repo,'packages','organization','examples','mosaic-stack.example.json'),'utf8'); export function biz(e,plugins){ const b=JSON.parse(EXAMPLE);let n=0; const walk=(o)=>{for(const [k,v] of Object.entries(o)){if(k==='botId')o[k]=++n;else if(v==='YYYY-MM-DD')o[k]='2027-01-01';else if(v&&typeof v==='object')walk(v)}}; walk(b);b.id='acme';if(plugins!==undefined)b.plugins=plugins; const f=join(dirname(e.cfg),'businesses','acme.json');mkdirSync(dirname(f),{recursive:true}); writeFileSync(f,JSON.stringify(b,null,2)+'\n',{mode:0o600});chmodSync(f,0o600); } export async function mk(t,dataName='data'){ const root=realpathSync(mkdtempSync(join(tmpdir(),'sr-'))); const e={root,data:join(root,dataName),bin:join(root,'bin'),sd:join(root,'sd'),units:join(root,'xcfg','systemd','user'),cfg:join(root,'cfg','config.json'),claude:join(root,'claude-bin','claude')}; // The default instance's names; named() sets NAME's (row 190). Object.assign(e,{flags:[],bus:BUS,tmpl:'mosaic-bus@.service',con:CON,wrapper:'mosaic'}); Object.assign(e,{state:join(e.data,'state'),pointer:join(e.data,'state','stack-active.json'),log:join(e.data,'state','stack-activation-log.jsonl'),store:join(e.data,'bus','bus.sqlite'),lock:join(e.data,'state','stack-release.lock')}); for(const d of [join(root,'home'),dirname(e.cfg),join(e.data,'bus'),e.bin,dirname(e.claude),join(e.sd,'faults'),e.units,join(root,'xdata')])mkdirSync(d,{recursive:true}); chmodSync(join(e.data,'bus'),0o700); writeFileSync(e.cfg,JSON.stringify({configVersion:1,environment:'development',dataRoot:e.data,execution:{backend:'docker',provider:'zai',model:'glm-5.3-flash',adapter:'mock'}})); biz(e); writeFileSync(join(e.bin,'mosaic'),`#!/usr/bin/env bash\n${MARKER}\n[ "\${1:-}" = __lease-capability ] && exec "${e.bin}/mosaic-old" "$@"\nexec "/old/scripts/mosaic" "$@"\n`,{mode:0o755}); writeFileSync(join(e.bin,'mosaic-old'),'#!/bin/sh\nexit 0\n',{mode:0o755}); writeFileSync(e.claude,'#!/bin/sh\necho "9.9.9 (Claude Code fixture)"\n',{mode:0o755}); e.env={PATH:`${SBIN}:${dirname(NODE)}:/usr/bin:/bin`,HOME:join(root,'home'),XDG_CONFIG_HOME:join(root,'xcfg'),XDG_DATA_HOME:join(root,'xdata'),MOSAIC_CONFIG:e.cfg,SD_STATE:e.sd,SD_DATA:e.data,TMPDIR:tmpdir()}; const server=await healthz(e); t.after(()=>{ for(const pid of lines(join(e.sd,'pids.log')).map(Number)){ try{if(readFileSync(`/proc/${pid}/cmdline`,'utf8').includes(join(base,'holder.cjs')))process.kill(pid,'SIGKILL')}catch{} } for(const s of [server,...(e.servers??[])])s.close(); execFileSync('chmod',['-R','u+w',root]);rmSync(root,{recursive:true,force:true}); }); return e; } // The Console's /healthz on e.port: 200 while the stand-in has e.con active and no healthz- // fault names its copy. export async function healthz(e){ const server=createServer((q,r)=>{ let st={};try{st=units(e)[e.con]||{}}catch{} const ok=q.url==='/healthz'&&st.state==='active'&&!existsSync(join(e.sd,'faults',`healthz-${basename(st.wd||'')}`)); r.writeHead(ok?200:500);r.end(); }); server.listen(0,'127.0.0.1');await once(server,'listening');e.port=String(server.address().port); return server; } // Row 190: stack instance NAME on e's host. It shares e's stand-ins, unit directory and $BIN, and has // its own config and data root from bootstrap --instance, its own Console port and business acme. // Its runs pass --instance NAME and no MOSAIC_CONFIG. export async function named(e,name){ const env={...e.env};delete env.MOSAIC_CONFIG; execFileSync('bash',[join(repo,'scripts','bootstrap.sh'),'--instance',name],{env,stdio:'ignore'}); const data=join(env.HOME,`.mosaic-${name}`); const n={...e,env,data,cfg:join(env.HOME,'.config',`mosaic-${name}`,'config.json'),flags:['--instance',name],bus:`mosaic-bus-${name}@acme.service`,tmpl:`mosaic-bus-${name}@.service`,con:`mosaic-webui-${name}.service`,wrapper:`mosaic-${name}`,name}; Object.assign(n,{state:join(data,'state'),pointer:join(data,'state','stack-active.json'),log:join(data,'state','stack-activation-log.jsonl'),store:join(data,'bus','bus.sqlite'),lock:join(data,'state','stack-release.lock')}); mkdirSync(join(data,'bus'),{mode:0o700}); biz(n); (e.servers??=[]).push(await healthz(n)); return n; } export async function run(e,...args){ const before=existsSync(e.data)?kept(e):[]; const ch=spawn('bash',[e.sr??SR,...args,...e.flags,'--copies',COPIES,'--bin',e.bin],{env:e.env,stdio:['ignore','pipe','pipe']}); let out='',err='';ch.stdout.on('data',(d)=>{out+=d});ch.stderr.on('data',(d)=>{err+=d}); const timer=setTimeout(()=>ch.kill('SIGKILL'),120000); const [code]=await once(ch,'close');clearTimeout(timer); const now=new Set(kept(e)); for(const p of before)assert.ok(now.has(p),`T17: ${args.join(' ')} removed ${p}`); return {code,out,err}; } // The stack on copy `copy` for business acme, both units active. pointer: adopt (a real adopt and // its log line), hand (written here, no log) or none. Snapshots start after the seed. export async function seed(e,{copy=B,pm=false,store='3g',pointer:how='adopt'}={}){ const db=new DatabaseSync(e.store); db.exec('PRAGMA journal_mode=WAL'); db.exec('CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)'); db.exec('CREATE TABLE events (id INTEGER PRIMARY KEY, at TEXT NOT NULL)'); db.prepare("INSERT INTO meta VALUES ('schema_version', ?)").run(store); for(let i=0;i<3;i++)db.prepare('INSERT INTO events (at) VALUES (?)').run(`2000-01-01T00:00:0${i}.000Z`); db.close(); const sh=(name,...a)=>execFileSync('bash',[join(repo,'scripts',name),...a],{env:e.env,encoding:'utf8',stdio:['ignore','pipe','pipe']}); sh('bus-service.sh','install',...e.flags,'--copy',copy,'--copies',COPIES,...(pm?['--pm']:[]),'--claude',e.claude); sh('webui-service.sh','install',...e.flags,'--copy',copy,'--copies',COPIES,'--port',e.port,'--business','acme'); sh('stack-copy.sh','wrapper',copy,...e.flags,'--copies',COPIES,'--bin',e.bin); // The seeded host started two minutes ago, so its boot status and PM run predate any run's start. execFileSync(join(SBIN,'systemctl'),['--user','start',e.bus],{env:{...e.env,SD_CLOCK_OFFSET:'120'}}); sysctl(e,'start',e.con); if(how==='adopt'){const r=await run(e,'adopt','acme');assert.equal(r.code,0,r.err)} else if(how==='hand'){ mkdirSync(e.state,{recursive:true}); writeFileSync(e.pointer,JSON.stringify({copy,business:'acme',pm,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g',activation:'manual',at:'2026-10-18T00:00:00Z'})+'\n'); } writeFileSync(join(e.sd,'snap.jsonl'),''); } export const rolledBack=(code,to=B)=>`stack-release: ${code}; rolled back to ${to}, which passed the gate\n`; export const planBackup=(e)=>`backup: the run backs up the stopped store to ${e.data}/backups/stack// before any migration`; export const planNoBackup='backup: none; the run rolls back before the backup'; export const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off','send: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];