Files
stack/packages/cli/tests/stack-release.test.mjs
T
rocko b2b33aee55 row 178 (#1676, P19 385): the Console checks a proxy proof header on public Hosts
webui serve takes --proof-header NAME --proof-file PATH, only with the --public-host preview flags.
Before listen it reads the file once through O_NOFOLLOW and fstat: a regular file owned by the
Console uid, mode 0400 or 0600, at least 32 base64url bytes with one optional final LF. Any file
problem gives one fixed refusal that names neither the path nor the contents. The header name can't
collide with Host, Origin, auth or tester names.

On a public Host the guard runs before any auth header is read. It needs exactly one proof header
and compares equal-length buffers with timingSafeEqual. Missing, wrong and repeated proofs get the
same fixed 401. A proof header on another Host or on the tester listener gets 403. The existing
auth, Origin, Fetch Metadata and human write checks are unchanged.

webui-service.sh renders both flags and checks the file's metadata and canonical location without
opening it, and the send-status regex accepts the proof pair. stack-release.sh only gains a comment:
it already carries Console flags through re-pin and rollback, and new tests pin that.

This is a partial fix. Same-uid seats can still read the proof file. Activation (middleware, unit
flags, restart, smoke) is a separate Mos-checked window and isn't part of this commit.

Dewey approved round 2 in #1676 comment 29386, candidate 9300a670. Round 1 changes were in 29374.
2026-10-20 00:37:08 -05:00

1312 lines
93 KiB
JavaScript

// scripts/stack-release.sh (D1, #1659). Design, the T table and the mutants:
// agents/darkwing/work/queue-162/BUILD.md. Every run uses a temp data root, temp units and stand-in
// systemctl, journalctl and systemd-analyze on PATH; nothing here reads ~/.mosaic-dev or a live unit.
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,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';
const ROOT=fileURLToPath(new URL('../../..',import.meta.url)).replace(/\/$/,'');
const NODE=process.execPath;
const MARKER='# mosaic-cli wrapper (Q81, agents/sage/work/q81/mosaic-cli.sh)';
const BUS='[email protected]',CON='mosaic-webui.service';
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();
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-<copy> fault
// names another case. Faults are files in $SD_STATE/faults named for the copy they hit.
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');
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}};
const isBus=(u)=>/^mosaic-bus@[a-z0-9-]+\.service$/.test(u),inst=(u)=>u.slice('mosaic-bus@'.length,-'.service'.length);
const unitFile=(u)=>{
const main=isBus(u)?'[email protected]':u,file=path.join(unitDir,main);
if(!fs.existsSync(file))return null;
const drops=[];
for(const d of isBus(u)?[main+'.d',u+'.d']:[u+'.d']){const dir=path.join(unitDir,d);if(fs.existsSync(dir))for(const f of fs.readdirSync(dir).sort())if(f.endsWith('.conf'))drops.push(path.join(dir,f))}
const r={wd:'',exec:'',env:[],drops};
for(const f of [file,...drops])for(let line of fs.readFileSync(f,'utf8').split('\n')){
line=line.replace(/%i/g,isBus(u)?inst(u):'').replace(/%h/g,process.env.HOME);
const m=/^(WorkingDirectory|ExecStart|Environment)=(.*)$/.exec(line);if(!m)continue;
if(m[1]==='WorkingDirectory')r.wd=m[2];else if(m[1]==='ExecStart'){if(m[2])r.exec=m[2]}else r.env.push(m[2]);
}
return r;
};
const snap=(ev)=>{
const dir=path.join(data,'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=(b,id,pid,off)=>{
let mode='';try{mode=fs.readFileSync(faultFile(`pm-${id}`),'utf8').trim()}catch{}
const now=Date.now()-off*1000,epoch=Math.floor(now/1000),at=new Date(now).toISOString(),run=`run-${pid}`,unit=`mosaic-bus@${b}.service`;
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=path.join(sd,'launches.json');let list=[];try{list=JSON.parse(fs.readFileSync(lf,'utf8'))}catch{}
const entry=(o)=>list.push({run,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;
default:ok();entry({});
}
fs.writeFileSync(lf,JSON.stringify(list));
};
if(verb==='show'){
const u=rest[0],props=[];for(let i=1;i<rest.length;i++)if(rest[i]==='-p')props.push(rest[++i]);
const f=unitFile(u),st=load()[u]||{},id=f?path.basename(f.wd):'';
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(' '):''}`);
}
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);
if(!isBus(u)){s[u]={state:'active',pid:null,wd:f.wd};save(s);process.exit(0)}
if(fault(`start-dies-${id}`)){s[u]={state:'failed',pid:null,wd:f.wd};save(s);process.exit(0)}
snap('start');
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'),data,inst(u),sd,off],{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};save(s);
if(f.exec.split(' ').slice(1).includes('--pm'))pm(inst(u),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()<end)sleep(20);
if(alive(st.pid))try{process.kill(st.pid,'SIGKILL')}catch{}
}
if(st){s[u]={...st,state:'inactive',pid:null};save(s)}
if(isBus(u)){
// One-shot: the stop leaves an empty -wal or -shm beside the store, as an unclean close would.
for(const [name,ext] of [['stop-leaves-wal','-wal'],['stop-leaves-shm','-shm']])if(fault(name)){fs.rmSync(faultFile(name));fs.writeFileSync(path.join(data,'bus','bus.sqlite'+ext),'')}
// One-shot (T45): the stop writes one row after any STAMP into the table the fault names, as a
// write the bus made between rollback's count and its stop would. The close leaves no -wal.
if(fault('stop-writes-row')){
const t=fs.readFileSync(faultFile('stop-writes-row'),'utf8');fs.rmSync(faultFile('stop-writes-row'));
const db=new (process.getBuiltinModule('node:sqlite').DatabaseSync)(path.join(data,'bus','bus.sqlite'));
db.exec(`CREATE TABLE IF NOT EXISTS ${t} (seq INTEGER PRIMARY KEY, at TEXT NOT NULL)`);
db.prepare(`INSERT INTO ${t} (at) VALUES (?)`).run('2999-01-01T00:00:00.000Z');db.close();
}
snap('stop');
}
process.exit(0);
}
console.error(`fixture systemctl: unsupported: ${args.join(' ')}`);process.exit(1);
}
// journalctl --user -u UNIT --since @EPOCH -o cat. A bare local time gets exit 1, as M35 needs.
function journalctl(){
const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path');
const sd=process.env.SD_STATE,a=process.argv.slice(2);let unit='',since='';
for(let i=0;i<a.length;i++){if(a[i]==='-u')unit=a[++i];else if(a[i]==='--since')since=a[++i]}
fs.appendFileSync(path.join(sd,'journal-since.log'),since+'\n');
if(fs.existsSync(path.join(sd,'faults','journal-fails')))process.exit(1);
let from;
if(/^@\d+$/.test(since))from=Number(since.slice(1));
else{const m=/^(\d{4}-\d\d-\d\d) (\d\d:\d\d:\d\d) UTC$/.exec(since);if(!m)process.exit(1);from=Date.parse(`${m[1]}T${m[2]}Z`)/1000}
let lines=[];try{lines=fs.readFileSync(path.join(sd,'journal.jsonl'),'utf8').split('\n').filter(Boolean).map((l)=>JSON.parse(l))}catch{}
for(const o of lines)if(o.unit===unit&&o.epoch>=from)console.log(o.text);
}
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.
function holder(){
const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path'),{DatabaseSync}=process.getBuiltinModule('node:sqlite');
const [data,business,sd,offset]=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(),host=path.join(sd,'host.json');
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.
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));
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);
let s={host:null,boot:null};
try{s=JSON.parse(fs.readFileSync(path.join(sd,'host.json'),'utf8'));s.host.live=alive(s.host.pid)}catch{}
console.log(JSON.stringify(s));
}else if(a==='bus status')console.log(`bus: fixture status for ${id}`);
else if(a==='launches list --json'){
if(fault(`launches-${id}`))process.exit(1);
let l=[];try{l=JSON.parse(fs.readFileSync(path.join(sd,'launches.json'),'utf8'))}catch{}
console.log(JSON.stringify(l));
}else{console.error(`fixture mosaic: ${a}`);process.exit(4)}
}
// A copy's migrate-schema.mjs: records the backups it saw, then sets 3h, or 3h-partial and exit 1.
function migrateStub(){
const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path'),{DatabaseSync}=process.getBuiltinModule('node:sqlite');
const data=process.argv[2],sd=process.env.SD_STATE,dir=path.join(data,'backups','stack');
const backups=fs.existsSync(dir)?fs.readdirSync(dir).sort().map((name)=>({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. ---
const base=realpathSync(mkdtempSync(join(tmpdir(),'stack-release-')));
after(()=>{execFileSync('chmod',['-R','u+w',base]);rmSync(base,{recursive:true,force:true})});
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');
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'],{encoding:'utf8'}).trim().split('\n');
for(const f of tracked)if(existsSync(join(ROOT,f)))cpSync(join(ROOT,f),join(repo,f));
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');
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.
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);const S=commit('row 905: S');
git(repo,'checkout','-q','-b','unreached',A);const U=commit('row 906: U');
git(repo,'checkout','-q','refactor');git(repo,'merge','-q','--no-ff','-m','merge side','side');
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).
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 and roles, which the tool imports to read the business file.
const KINDS_NEW=readFileSync(join(repo,'packages','plugins','src','kinds.mjs'),'utf8');
const KINDS_OLD=KINDS_NEW.replace('\n harness: Object.freeze({','\n "harness-before-c216f47db": Object.freeze({');
assert.notEqual(KINDS_OLD,KINDS_NEW);
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":'// 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]]){
mkdirSync(dirname(join(d,f)),{recursive:true});writeFileSync(join(d,f),text,{mode});
}
for(const p of ['packages/business/src','roles'])cpSync(join(repo,p),join(d,p),{recursive:true});
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`);
const SR=join(repo,'scripts','stack-release.sh');
// --- Per-test roots and helpers ---
const sha=(f)=>createHash('sha256').update(readFileSync(f)).digest('hex');
const lines=(f)=>existsSync(f)?readFileSync(f,'utf8').split('\n').filter(Boolean):[];
const calls=(e)=>lines(join(e.sd,'calls.log'));
const startsStops=(e,from)=>calls(e).slice(from).filter((c)=>/^(start|stop) /.test(c));
const logLines=(e)=>lines(e.log).map((l)=>JSON.parse(l));
const pointer=(e)=>existsSync(e.pointer)?JSON.parse(readFileSync(e.pointer,'utf8')):null;
const units=(e)=>JSON.parse(readFileSync(join(e.sd,'units.json'),'utf8'));
const snaps=(e)=>lines(join(e.sd,'snap.jsonl')).map((l)=>JSON.parse(l));
const fault=(e,name,text='')=>writeFileSync(join(e.sd,'faults',name),text);
const sysctl=(e,...a)=>execFileSync(join(SBIN,'systemctl'),['--user',...a],{env:e.env,encoding:'utf8'});
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))};
const posts=(e)=>readdirSync(e.data).filter((n)=>n.startsWith('bus.post-attempt-'));
const backupSum=(e,act)=>readFileSync(join(e.data,'backups','stack',act,'SHA256SUMS'),'utf8').split(' ')[0];
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.
const fp=(e)=>({bus:sha(join(e.units,'[email protected]')),console:sha(join(e.units,CON)),wrapper:sha(join(e.bin,'mosaic')),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.
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.
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.
const kept=(e)=>[...tree(join(e.data,'backups','stack')),...tree(join(e.state,'stack-rollback')),...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.
const EXAMPLE=readFileSync(join(repo,'packages','business','examples','mosaic-stack.example.json'),'utf8');
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);
}
async function mk(t){
const root=realpathSync(mkdtempSync(join(tmpdir(),'sr-')));
const e={root,data:join(root,'data'),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')};
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()};
// The Console's /healthz: 200 while the stand-in has it active and no healthz-<copy> fault names its copy.
const server=createServer((q,r)=>{
let st={};try{st=units(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);
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{}
}
server.close();execFileSync('chmod',['-R','u+w',root]);rmSync(root,{recursive:true,force:true});
});
return e;
}
async function run(e,...args){
const before=existsSync(e.data)?kept(e):[];
const ch=spawn('bash',[e.sr??SR,...args,'--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.
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','--copy',copy,'--copies',COPIES,...(pm?['--pm']:[]),'--claude',e.claude);
sh('webui-service.sh','install','--copy',copy,'--copies',COPIES,'--port',e.port,'--business','acme');
sh('stack-copy.sh','wrapper',copy,'--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',BUS],{env:{...e.env,SD_CLOCK_OFFSET:'120'}});
sysctl(e,'start',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'),'');
}
const rolledBack=(code,to=B)=>`stack-release: ${code}; rolled back to ${to}, which passed the gate\n`;
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,`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}`];
// --- The T table ---
test('T1 adopt writes the pointer from the units once; a failing check refuses check-failed',async(t)=>{
const e=await mk(t);await seed(e,{pointer:'none'});
const r=await run(e,'adopt','acme');assert.equal(r.code,0,r.err);
assert.equal(r.out,`adopted ${B} for acme (pm 0, schema 3g)\n`);
const {at,activation,...p}=pointer(e);
assert.deepEqual(p,{copy:B,business:'acme',pm:false,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
assert.match(activation,new RegExp(`^\\d{8}T\\d{6}Z-${B.slice(0,12)}$`));assert.match(at,/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/);
assert.deepEqual(logLines(e).map((l)=>[l.op,l.from,l.to,l.result]),[['adopt',null,B,'ok']]);
const again=await run(e,'adopt','acme');assert.equal(again.code,3);
assert.equal(again.err,`stack-release: refusing already-adopted: ${e.pointer} exists (copy ${B})\n`);
assert.equal(logLines(e).at(-1).code,'already-adopted');
const f=await mk(t);await seed(f,{pointer:'none'});fault(f,`check-${B}`);
const bad=await run(f,'adopt','acme');assert.equal(bad.code,3);
assert.match(bad.err,/^stack-release: refusing check-failed: stack-copy.sh check did not pass:\nbus: /);
assert.equal(pointer(f),null);
});
test('T2 activate installs exactly the pointer\'s pm, claude and Console flags',async(t)=>{
const e=await mk(t);await seed(e);
const r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.err+r.out);
assert.ok(r.out.endsWith(`stack-release: activate ok: ${C1} is live for acme\n`),r.out);
const bus=readFileSync(join(e.units,'[email protected]'),'utf8').split('\n'),con=readFileSync(join(e.units,CON),'utf8').split('\n');
assert.ok(bus.includes(`ExecStart=${COPIES}/${C1}/scripts/mosaic bus start %i`),bus.join('\n'));
assert.ok(bus.includes(`Environment=MOSAIC_CLAUDE_BIN=${e.claude}`));
const exec=con.find((l)=>l.startsWith('ExecStart='));
assert.ok(exec.endsWith(` ${COPIES}/${C1}/packages/webui/src/cli.mjs serve --checkout ${repo} --port ${e.port} --business acme`),exec);
assert.equal(readFileSync(join(e.bin,'mosaic'),'utf8').trimEnd().split('\n').at(-1),`exec "${COPIES}/${C1}/scripts/mosaic" "$@"`);
const {at,activation,...p}=pointer(e);
assert.deepEqual(p,{copy:C1,business:'acme',pm:false,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
});
test('T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing',async(t)=>{
const cases=[
['pm',(e)=>edit(join(e.units,'[email protected]'),' bus start %i\n',' bus start %i --pm\n'),()=>' pm: pointer 0, unit 1'],
['claude',(e)=>{const d=join(e.units,`${BUS}.d`);mkdirSync(d);writeFileSync(join(d,'claude.conf'),'[Service]\nEnvironment=MOSAIC_CLAUDE_BIN=/opt/other/claude\n')},(e)=>` claude: pointer ${e.claude}, unit /opt/other/claude`],
['console',(e)=>edit(join(e.units,CON),'--business acme\n','--business acme --board http://127.0.0.1:9\n'),(e)=>` console: pointer --port ${e.port} --business acme, unit --port ${e.port} --business acme --board http://127.0.0.1:9`],
];
for(const [name,change,line] of cases)await t.test(name,async(t)=>{
const e=await mk(t);await seed(e);change(e);
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
const r=await run(e,'activate',C1,'acme');assert.equal(r.code,3);
assert.equal(r.err,`stack-release: refusing unit-flags-drift: the units differ from the pointer; settle it first:\n${line(e)}\n`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
assert.equal(logLines(e).length,logs+1);assert.equal(logLines(e).at(-1).code,'unit-flags-drift');
});
});
test('T4 each item 3 refusal changes nothing and logs one line with its code',async(t)=>{
const setUnit=(e,u,state)=>{const s=units(e);s[u]={...(s[u]||{pid:null,wd:''}),state};writeFileSync(join(e.sd,'units.json'),JSON.stringify(s))};
const cases=[
['copy-missing',()=>M,()=>`no copy ${M} in ${COPIES}`],
['copy-unreachable',()=>U,()=>`${U} is not reachable from refactor and origin/refactor`],
['copy-schema-unknown',()=>F,()=>`cannot read SCHEMA_VERSION and its chain from ${COPIES}/${F}/packages/bus/src/store.mjs`],
['already-active',()=>B,()=>`${B} is the pointer's copy`],
['instance-busy',(e)=>{setUnit(e,'[email protected]','active');return C1},()=>'[email protected] is active; the copy and wrapper switch reach it too','another business active'],
['instance-busy',(e)=>{setUnit(e,BUS,'activating');return C1},()=>`${BUS} is activating`,'this business activating'],
['drop-in-override',(e)=>{const d=join(e.units,`${BUS}.d`);mkdirSync(d);writeFileSync(join(d,'override.conf'),'[Service]\nWorkingDirectory=/elsewhere\n');return C1},(e)=>`${join(e.units,`${BUS}.d`,'override.conf')} overrides ExecStart or WorkingDirectory; the operator removes it as in TOOLS.md step 4`],
['no-pointer',()=>C1,(e)=>`no ${e.pointer}; run adopt first`,'',{pointer:'none'}],
];
for(const [code,setup,msg,label,opts] of cases)await t.test(label?`${code} (${label})`:code,async(t)=>{
const e=await mk(t);await seed(e,opts);const id=setup(e);
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
const r=await run(e,'activate',id,'acme');assert.equal(r.code,3,r.err);
assert.equal(r.err,`stack-release: refusing ${code}: ${msg(e)}\n`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
assert.equal(logLines(e).length,logs+1);assert.deepEqual([logLines(e).at(-1).result,logLines(e).at(-1).code],['refused',code]);
});
});
test('T5 an activate with no migration stops the Console then the bus, installs, starts the bus then the Console',async(t)=>{
const e=await mk(t);await seed(e);const n=calls(e).length;
const r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.err);
assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]);
const c=calls(e).slice(n),reload=c.indexOf('daemon-reload');
assert.ok(reload>c.indexOf(`stop ${BUS}`)&&reload<c.indexOf(`start ${BUS}`),'daemon-reload between the stops and the starts');
assert.equal(pointer(e).copy,C1);assert.equal(units(e)[BUS].wd,join(COPIES,C1));
assert.ok(!existsSync(join(e.data,'backups','stack')));
const l=logLines(e).slice(-2);assert.deepEqual(l.map((x)=>[x.op,x.from,x.to,x.result]),[['activate',B,C1,'started'],['activate',B,C1,'ok']]);
});
test('T6 a migration without --migrate rolls back with migration-needs-flag',async(t)=>{
const e=await mk(t);await seed(e);const before=fp(e);
const r=await run(e,'activate',C,'acme','--timeout','3');assert.equal(r.code,1,r.out);
assert.equal(r.err,rolledBack('migration-needs-flag'));
assert.deepEqual(fp(e),before);
assert.deepEqual([units(e)[BUS].state,units(e)[BUS].wd,units(e)[CON].state],['active',join(COPIES,B),'active']);
const l=logLines(e).at(-1);assert.deepEqual([l.result,l.code,l.rollback],['gate-failed','migration-needs-flag','ok']);
assert.equal(storeVersion(e.store,true),'3g');assert.ok(!existsSync(join(e.data,'backups','stack')));
});
test('T7 --migrate takes the backup before the migration runs',async(t)=>{
const e=await mk(t);await seed(e);const before=fp(e);
const r=await run(e,'activate',C,'acme','--migrate');assert.equal(r.code,0,r.err+r.out);
const act=pointer(e).activation;
assert.deepEqual(JSON.parse(readFileSync(join(e.sd,'migrate.json'),'utf8')),{backups:[{name:act,sums:true}]});
assert.equal(backupSum(e,act),before.store);
assert.deepEqual([pointer(e).copy,pointer(e).schema],[C,'3h']);assert.equal(storeVersion(e.store,true),'3h');
assert.equal(logLines(e).at(-1).backup,act);
});
test('T8 a failed migration restores the backup and keeps the store it replaced',async(t)=>{
const e=await mk(t);await seed(e);const before=fp(e);fault(e,'migrate-fails');
const r=await run(e,'activate',C,'acme','--migrate','--timeout','3');assert.equal(r.code,1,r.out);
assert.equal(r.err,rolledBack('migrate-failed'));
assert.equal(posts(e).length,1);assert.equal(storeVersion(join(e.data,posts(e)[0],'bus.sqlite'),false),'3h-partial');
const act=logLines(e).at(-1).activation;
assert.equal(sha(e.store),backupSum(e,act));assert.equal(sha(e.store),before.store);
assert.deepEqual(fp(e),before);assert.equal(units(e)[BUS].wd,join(COPIES,B));
});
test('T9 a store outside the target\'s chain rolls back with store-newer',async(t)=>{
const e=await mk(t);await seed(e,{store:'3z'});const before=fp(e);
const r=await run(e,'activate',C,'acme','--migrate','--timeout','3');assert.equal(r.code,1,r.out);
assert.equal(r.err,rolledBack('store-newer'));
assert.deepEqual(fp(e),before);assert.equal(storeVersion(e.store,true),'3z');assert.ok(!existsSync(join(e.data,'backups','stack')));
});
test('T10 each gate check failing alone rolls back and puts every file back',async(t)=>{
const cases=[['units-inactive',`start-dies-${C1}`],['bus-status',`status-${C1}`],['healthz',`healthz-${C1}`],['copy-check',`check-${C1}`],['pm-launch-unconfirmed',`pm-${C1}`,'none']];
for(const [code,name,text] of cases)await t.test(code,async(t)=>{
const pm=code==='pm-launch-unconfirmed',e=await mk(t);await seed(e,{pm});fault(e,name,text);
const before=fp(e);
const r=await run(e,'activate',C1,'acme',...(pm?['--pm']:[]),'--timeout','3');assert.equal(r.code,1,r.out+r.err);
assert.equal(r.err,rolledBack(code));assert.ok(r.out.includes(`stack-release: gate failed on ${C1}: ${code}`),r.out);
assert.deepEqual(fp(e),before);
assert.deepEqual([units(e)[BUS].state,units(e)[BUS].wd],['active',join(COPIES,B)]);
const l=logLines(e).at(-1);assert.deepEqual([l.result,l.code,l.rollback],['gate-failed',code,'ok']);
});
});
test('T10b the PM check needs exactly one launched line for a live run of this start',async(t)=>{
const cases=[
['no launched line','none',/\(no launched line since the start\)/],
['two lines','two',/\(2 launched lines since the start\)/],
['a changed word','badword',/\(the launched line does not parse: launched pm-acme: job run-\d+, claude opus\)/],
['run not in launches list','notlisted',/\(run run-\d+ is not in launches list\)/],
['run started before the start','early',/\(run run-\d+ started before this start\)/],
['a human launch with no journal line','human',/\(no launched line since the start\)/],
['run for another business','other',/\(run run-\d+ is for other\)/],
['run not live','dead',/\(run run-\d+ is not live\)/],
];
for(const [name,mode,why] of cases)await t.test(name,async(t)=>{
const e=await mk(t);await seed(e,{pm:true});fault(e,`pm-${C1}`,mode);
const r=await run(e,'activate',C1,'acme','--pm','--timeout','3');assert.equal(r.code,1,r.out+r.err);
assert.equal(r.err,rolledBack('pm-launch-unconfirmed'));
const gate=r.out.split('\n').find((l)=>l.startsWith(`stack-release: gate failed on ${C1}: pm-launch-unconfirmed`));
assert.ok(gate,r.out);assert.match(gate,why);
});
await t.test('one well-formed line and a live run pass',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});
const r=await run(e,'activate',C1,'acme','--pm');assert.equal(r.code,0,r.out+r.err);
assert.ok(readFileSync(join(e.units,'[email protected]'),'utf8').includes(`ExecStart=${COPIES}/${C1}/scripts/mosaic bus start %i --pm\n`));
assert.match(lines(join(e.sd,'journal-since.log')).at(-1),/^@\d+$/);
assert.equal(pointer(e).pm,true);
});
});
test('T11 a rollback whose gate fails too: rollback-failed, exit 2, both units stopped, no retry',async(t)=>{
const e=await mk(t);await seed(e);fault(e,`healthz-${C1}`);fault(e,`healthz-${B}`);const n=calls(e).length;
const r=await run(e,'activate',C1,'acme','--timeout','3');assert.equal(r.code,2,r.out+r.err);
const l=logLines(e).at(-1),rs=join(e.state,'stack-rollback',l.activation);
assert.equal(r.err,`stack-release: rollback-failed: the run failed with healthz and the rollback with healthz; both units are stopped. Rollback set: ${rs}\n`);
assert.deepEqual([l.result,l.code,l.rollback,l.rollbackCode],['rollback-failed','healthz','failed','healthz']);
assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['inactive','inactive']);
const ss=startsStops(e,n);
assert.deepEqual(ss.filter((c)=>c.startsWith('start ')),[`start ${BUS}`,`start ${CON}`,`start ${BUS}`,`start ${CON}`]);
assert.deepEqual(ss.slice(-2),[`stop ${CON}`,`stop ${BUS}`]);
assert.equal(pointer(e).copy,B);
});
test('T12 rollback takes the previous copy from the log',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C1,'acme')).code,0);
const r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
assert.ok(r.out.endsWith(`stack-release: rollback ok: ${B} is live for acme\n`),r.out);
assert.equal(pointer(e).copy,B);
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.from,l.to,l.result],['rollback',C1,B,'ok']);
});
test('T13 a rollback across a migration needs --restore, then restores and goes live',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation,storeC=sha(e.store);
const no=await run(e,'rollback','acme');assert.equal(no.code,3);
assert.equal(no.err,`stack-release: refusing schema-differs: the store is at 3h and ${B} expects 3g; pass --restore ACTIVATION\n`);
const r=await run(e,'rollback','acme','--restore',act);assert.equal(r.code,0,r.out+r.err);
const o=join(e.data,'backups','stack',act);
assert.match(r.out,new RegExp(`^restore: ${o}; the store now in place moves to ${e.data}/bus\\.post-attempt-\\d{8}T\\d{6}Z and is kept; 0 event\\(s\\) were written after the backup\\n`));
assert.deepEqual([pointer(e).copy,pointer(e).schema],[B,'3g']);assert.equal(storeVersion(e.store,true),'3g');
assert.equal(sha(e.store),backupSum(e,act));
assert.equal(posts(e).length,1);assert.equal(sha(join(e.data,posts(e)[0],'bus.sqlite')),storeC);
assert.equal(logLines(e).at(-1).backup,act);
});
test('T14 events written after the backup need --events N with the right N',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
const db=new DatabaseSync(e.store);for(let i=0;i<2;i++)db.prepare('INSERT INTO events (at) VALUES (?)').run('2999-01-01T00:00:00.000Z');db.close();
const want='stack-release: refusing post-backup-events: 2 event(s) were written after the backup; Jason decides, then --events 2\n';
for(const extra of [[],['--events','1']]){
const before=fp(e),r=await run(e,'rollback','acme','--restore',act,...extra);
assert.equal(r.code,3,extra.join(' '));assert.equal(r.err,want);assert.deepEqual(fp(e),before);
}
const r=await run(e,'rollback','acme','--restore',act,'--events','2');assert.equal(r.code,0,r.out+r.err);
assert.match(r.out,/; 2 event\(s\) were written after the backup\n/);
assert.deepEqual([pointer(e).copy,pointer(e).schema],[B,'3g']);
});
test('T14b a rollback --restore whose gate fails stops both units and skips item 6',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation,storeC=sha(e.store),before=fp(e);
fault(e,`healthz-${B}`);const n=calls(e).length;
const r=await run(e,'rollback','acme','--restore',act,'--timeout','3');assert.equal(r.code,2,r.out+r.err);
const l=logLines(e).at(-1),rs=join(e.state,'stack-rollback',l.activation),post=join(e.data,posts(e)[0]);
assert.equal(r.err,`stack-release: rollback-failed: the run failed with healthz and the rollback with not-attempted; both units are stopped. Rollback set: ${rs}, backup ${join(e.data,'backups','stack',act)}, post-attempt store ${post}\n`);
assert.deepEqual([l.result,l.code,l.rollbackCode],['rollback-failed','healthz','not-attempted']);
assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['inactive','inactive']);
assert.equal(posts(e).length,1);assert.equal(sha(join(post,'bus.sqlite')),storeC);assert.equal(sha(e.store),backupSum(e,act));
assert.deepEqual(startsStops(e,n).filter((c)=>c.startsWith('start ')),[`start ${BUS}`,`start ${CON}`]);
assert.equal(pointer(e).copy,C);assert.equal(sha(e.pointer),before.pointer);
});
test('T14c a rollback without --restore whose gate fails runs item 6',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C1,'acme')).code,0);const before=fp(e);
fault(e,`healthz-${B}`);
const r=await run(e,'rollback','acme','--timeout','3');assert.equal(r.code,1,r.out+r.err);
assert.equal(r.err,rolledBack('healthz',C1));
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.result,l.code,l.rollback],['rollback','gate-failed','healthz','ok']);
assert.deepEqual(fp(e),before);assert.deepEqual([units(e)[BUS].state,units(e)[BUS].wd],['active',join(COPIES,C1)]);
assert.deepEqual(readdirSync(join(e.data,'bus')).filter((n)=>n.startsWith('bus.sqlite')).sort(),['bus.sqlite','bus.sqlite-shm','bus.sqlite-wal']);
});
test('T15 --plan names the migration and the restore and changes nothing',async(t)=>{
const e=await mk(t);await seed(e);
const quiet=async(...args)=>{
const before=fp(e),n=calls(e).length,logs=logLines(e).length,r=await run(e,...args);
assert.equal(r.code,0,r.err);assert.equal(r.err,'');
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
return r.out;
};
assert.ok((await quiet('activate',C,'acme','--plan')).includes('needs: --migrate (not given; the run would roll back with migration-needs-flag)\n'));
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
assert.equal(await quiet('rollback','acme','--plan'),[
'plan: rollback for acme',`from: ${C}`,`to: ${B}`,
'store schema: 3h (read from the store the running bus holds)','target schema: 3g',
`restore: the store must go back to 3g; needs --restore ${act}`,
'commits: the run takes away 2 (newest first, at most 200):',...commitLines(`${B}..${C}`),
...planTail(e,B),''].join('\n'));
});
test('T26 --plan lists the commits an activate adds, after the migration lines',async(t)=>{
const e=await mk(t);await seed(e);
const head=(mig)=>['plan: activate for acme',`from: ${B}`,`to: ${C}`,'store schema: 3g (read from the store the running bus holds)','target schema: 3h',
'migration: 3g to 3h, after a backup of the stopped store','migration steps: 3g to 3h',`needs: --migrate (${mig})`,'needs: Jason present for this run (decision 96 condition 2)'];
const adds=commitLines(`${B}..${C}`);
assert.equal(adds.length,2);assert.match(adds[0],/^ {2}[0-9a-f]{7,} row 904: C$/);assert.match(adds[1],/^ {2}[0-9a-f]{7,} row 903: C1$/);
for(const [flags,mig] of [[[],'not given; the run would roll back with migration-needs-flag'],[['--migrate'],'given']]){
const r=await run(e,'activate',C,'acme','--plan',...flags);assert.equal(r.code,0,r.err);
assert.equal(r.out,[...head(mig),'commits: the run adds 2 (newest first, at most 200):',...adds,...planTail(e,C),''].join('\n'));
}
const same=await run(e,'activate',C1,'acme','--plan');assert.equal(same.code,0,same.err);
assert.ok(same.out.includes('target schema: 3g\nmigration: none; the store is at the target\'s version\ncommits: the run adds 1 (newest first, at most 200):\n'),same.out);
});
test('T27 --plan names each migration step, or says the steps cannot be read',async(t)=>{
const e=await mk(t);await seed(e,{store:'3f',pointer:'hand'});
const two=await run(e,'activate',C,'acme','--plan');assert.equal(two.code,0,two.err);
assert.ok(two.out.includes('\nmigration: 3f to 3h, after a backup of the stopped store\nmigration steps: 3f to 3g, 3g to 3h\nneeds: --migrate'),two.out);
const none=await run(e,'activate',C1,'acme','--plan');assert.equal(none.code,0,none.err);
assert.ok(none.out.includes(`\nmigration: 3f to 3g, after a backup of the stopped store\nmigration steps: cannot be read from ${COPIES}/${C1}/packages/bus/src/store.mjs\nneeds: --migrate`),none.out);
const short=await run(e,'activate',A,'acme','--plan');assert.equal(short.code,0,short.err);
assert.ok(short.out.includes(`\nmigration: 3f to 3g, after a backup of the stopped store\nmigration steps: cannot be read from ${COPIES}/${A}/packages/bus/src/store.mjs\nneeds: --migrate`),short.out);
});
test('T25 --plan refuses copies-unrelated when neither copy is an ancestor of the other',async(t)=>{
const e=await mk(t);await seed(e,{pointer:'hand'});
const r=await run(e,'activate',S,'acme','--plan');assert.equal(r.code,3);
assert.equal(r.err,`stack-release: refusing copies-unrelated: neither ${B} nor ${S} is an ancestor of the other, so the plan cannot list the commits\n`);
assert.equal(r.out,'');assert.ok(!existsSync(e.log));assert.ok(!existsSync(e.lock));
});
test('T23 --plan reads the store without changing a file, stopped or held',async(t)=>{
const want=['target schema: 3h','migration: 3g to 3h, after a backup of the stopped store','migration steps: 3g to 3h','needs: --migrate (not given; the run would roll back with migration-needs-flag)','needs: Jason present for this run (decision 96 condition 2)'];
for(const [name,stop,src] of [['stopped store',true,'read from the stopped store'],['held store',false,'read from the store the running bus holds']])await t.test(name,async(t)=>{
const e=await mk(t);await seed(e,{pointer:'hand'});if(stop)sysctl(e,'stop',BUS);
const before=tree(e.data),units0=[sha(join(e.units,'[email protected]')),sha(join(e.units,CON)),sha(join(e.bin,'mosaic'))],store0=sha(e.store);
const r=await run(e,'activate',C,'acme','--plan');assert.equal(r.code,0,r.err);
const out=r.out.split('\n');
assert.ok(out.includes(`store schema: 3g (${src})`),r.out);
assert.deepEqual(out.slice(out.indexOf('target schema: 3h'),out.indexOf('target schema: 3h')+5),want);
assert.deepEqual(tree(e.data),before);assert.equal(sha(e.store),store0);
assert.deepEqual([sha(join(e.units,'[email protected]')),sha(join(e.units,CON)),sha(join(e.bin,'mosaic'))],units0);
assert.ok(!existsSync(e.log));assert.ok(!existsSync(e.lock));
if(stop)assert.deepEqual(readdirSync(join(e.data,'bus')),['bus.sqlite']);
});
});
test('T24 business-mismatch and capture-failed refuse with exit 3; a short id is a usage error',async(t)=>{
const e=await mk(t);await seed(e);
let n=calls(e).length,logs=logLines(e).length;
const r=await run(e,'activate',C1,'other');assert.equal(r.code,3);
assert.equal(r.err,'stack-release: refusing business-mismatch: the pointer is for acme\n');
assert.equal(logLines(e).length,logs+1);assert.equal(logLines(e).at(-1).code,'business-mismatch');assert.deepEqual(startsStops(e,n),[]);
const sets=join(e.state,'stack-rollback');mkdirSync(sets);chmodSync(sets,0o555);
n=calls(e).length;logs=logLines(e).length;const before=fp(e);
const c=await run(e,'activate',C1,'acme');assert.equal(c.code,3);
assert.match(c.err,new RegExp(`^stack-release: refusing capture-failed: could not write the rollback set ${sets}/\\d{8}T\\d{6}Z-${C1.slice(0,12)}; nothing was stopped\\n$`));
const l=logLines(e).slice(logs);
assert.deepEqual(l.map((x)=>[x.result,x.code]),[['started',undefined],['refused','capture-failed']]);assert.equal(l[0].activation,l[1].activation);
assert.deepEqual(startsStops(e,n),[]);assert.deepEqual(fp(e),before);
const s=await run(e,'status','acme');assert.equal(s.code,0,s.err);assert.ok(!s.out.includes('interrupted run'),s.out);
logs=logLines(e).length;
const u=await run(e,'activate','abc123','acme');assert.equal(u.code,4);assert.match(u.err,/^usage: stack-release.sh adopt BUSINESS\n/);
assert.equal(logLines(e).length,logs);
});
test('T16 status prints the pointer, the log, the check, bus status and drift, and writes nothing',async(t)=>{
const e=await mk(t);await seed(e);
const before=tree(e.data),fp0=fp(e);
const r=await run(e,'status','acme');assert.equal(r.code,0,r.err);
const copy=join(COPIES,B);
assert.equal(r.out,[
`pointer: ${e.pointer}`,...readFileSync(e.pointer,'utf8').trimEnd().split('\n').map((l)=>` ${l}`),
'log (last five lines):',...lines(e.log).map((l)=>` ${l}`),
'check:',` bus: ${copy}`,` Console: ${copy}`,` CLI: ${copy}`,' result: pass',
'bus status:',` bus: fixture status for ${B}`,
'unit flags: match the pointer',
'drift: 4 commit(s) on refactor since the pointer\'s copy',''].join('\n'));
assert.deepEqual(tree(e.data),before);assert.deepEqual(fp(e),fp0);
});
test('T19 a started line with no final line: status shows it and every changing verb refuses',async(t)=>{
const e=await mk(t);await seed(e);
const act=`20261018T000000Z-${C1.slice(0,12)}`;
appendFileSync(e.log,JSON.stringify({at:'2026-10-18T00:00:00Z',op:'activate',from:B,to:C1,backup:null,activation:act,result:'started'})+'\n');
const s=await run(e,'status','acme');assert.equal(s.code,0);
assert.ok(s.out.includes(`interrupted run: ${act} (started, no final line; Jason or Mos settles it)\n`),s.out);
for(const args of [['adopt','acme'],['activate',C1,'acme'],['rollback','acme']]){
const before=fp(e),n=calls(e).length,r=await run(e,...args);
assert.equal(r.code,3,args.join(' '));
assert.equal(r.err,`stack-release: refusing interrupted-run: activation ${act} started and has no final line; Jason or Mos settles it (status shows it)\n`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
}
});
test('T20 a held run lock refuses run-locked without a log line; status and --plan still run',async(t)=>{
const e=await mk(t);await seed(e);
const h=spawn('flock',[e.lock,'sleep','30'],{detached:true,stdio:'ignore'});
t.after(()=>{try{process.kill(-h.pid,'SIGKILL')}catch{}});
const end=Date.now()+10000;
while(spawnSync('flock',['-n',e.lock,'true']).status!==1){assert.ok(Date.now()<end,'the lock holder did not take the lock');await new Promise((r)=>setTimeout(r,50))}
for(const args of [['adopt','acme'],['activate',C1,'acme'],['rollback','acme']]){
const before=fp(e),logs=logLines(e).length,r=await run(e,...args);
assert.equal(r.code,3,args.join(' '));
assert.equal(r.err,`stack-release: refusing run-locked: another stack-release run holds ${e.lock}\n`);
assert.equal(logLines(e).length,logs);assert.deepEqual(fp(e),before);
}
assert.equal((await run(e,'status','acme')).code,0);
const p=await run(e,'activate',C1,'acme','--plan');assert.equal(p.code,0,p.err);
});
test('T21 a bus unit with no MOSAIC_CLAUDE_BIN: adopt and activate refuse no-claude-pin',async(t)=>{
const drop=(e)=>edit(join(e.units,'[email protected]'),`Environment=MOSAIC_CLAUDE_BIN=${e.claude}\n`,'');
const want=`stack-release: refusing no-claude-pin: ${BUS} has no MOSAIC_CLAUDE_BIN\n`;
const a=await mk(t);await seed(a,{pointer:'none'});drop(a);
const r=await run(a,'adopt','acme');assert.equal(r.code,3);assert.equal(r.err,want);assert.equal(pointer(a),null);
const b=await mk(t);await seed(b);drop(b);const before=fp(b),n=calls(b).length;
const s=await run(b,'activate',C1,'acme');assert.equal(s.code,3);assert.equal(s.err,want);
assert.deepEqual(fp(b),before);assert.deepEqual(startsStops(b,n),[]);
});
test('T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file',async(t)=>{
for(const ext of ['-wal','-shm'])await t.test(`bus.sqlite${ext}`,async(t)=>{
const e=await mk(t);await seed(e);fault(e,`stop-leaves${ext}`);
const r=await run(e,'activate',C1,'acme','--timeout','3');assert.equal(r.code,1,r.out+r.err);
assert.equal(r.err,rolledBack('store-unclean'));
assert.ok(r.out.includes(`stack-release: ${e.store}${ext} is present; the store is left as it is for Jason or Mos\n`),r.out);
const s=snaps(e),first=s[0],start=s.find((x)=>x.ev==='start');
assert.equal(first.ev,'stop');assert.ok(`bus.sqlite${ext}` in first.files,JSON.stringify(first));
assert.deepEqual(start.files,first.files);
});
await t.test('clean',async(t)=>{
const e=await mk(t);await seed(e);
const r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
const s=snaps(e),stop=s.find((x)=>x.ev==='stop'),start=s.find((x)=>x.ev==='start');
assert.deepEqual(Object.keys(stop.files),['bus.sqlite']);assert.deepEqual(start.files,stop.files);
});
});
// --- Row 166 (#1663): docs/plans/2026-10-18_stack-release-hardening.md items 1 to 4 ---
test('T28 a unit path, drop-in directory or drop-in file that cannot be read, or a relative unit path, refuses drop-in-override before any stop',async(t)=>{
const cases=[
['unit path',(e)=>[e.units,`cannot inspect unit path ${e.units}`]],
['drop-in directory',(e)=>{const d=join(e.units,`${BUS}.d`);mkdirSync(d);writeFileSync(join(d,'note.conf'),'[Service]\n');return [d,`cannot inspect drop-ins ${d}`]}],
['drop-in file',(e)=>{const d=join(e.units,`${BUS}.d`),f=join(d,'claude.conf');mkdirSync(d);writeFileSync(f,'[Service]\n');return [f,`cannot read drop-in ${f}`]}],
];
for(const [name,make] of cases)await t.test(name,async(t)=>{
const e=await mk(t);await seed(e);
const [p,msg]=make(e),before=fp(e),n=calls(e).length,logs=logLines(e).length,mode=statSync(p).mode&0o7777;
chmodSync(p,0);
let r;try{r=await run(e,'activate',C1,'acme')}finally{chmodSync(p,mode)}
assert.equal(r.code,3,r.out+r.err);assert.equal(r.err,`stack-release: refusing drop-in-override: ${msg}\n`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['active','active']);
assert.equal(logLines(e).length,logs+1);assert.equal(logLines(e).at(-1).code,'drop-in-override');
});
// install_guard refuses a unit path that isn't absolute, so the precheck does too.
await t.test('relative unit path',async(t)=>{
const e=await mk(t);await seed(e);fault(e,'unit-path','systemd/user');
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
const r=await run(e,'activate',C1,'acme');assert.equal(r.code,3,r.out+r.err);
assert.equal(r.err,'stack-release: refusing drop-in-override: invalid systemd unit path systemd/user\n');
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['active','active']);
assert.equal(logLines(e).length,logs+1);assert.equal(logLines(e).at(-1).code,'drop-in-override');
});
});
test('T29 a bus that stops during the held-store read: --plan says so, rollback --restore refuses restore-mismatch',async(t)=>{
await t.test('--plan',async(t)=>{
const e=await mk(t);await seed(e);fault(e,'bus-stops-after','1');
const before=fp(e),logs=logLines(e).length;
const r=await run(e,'activate',C,'acme','--plan');assert.equal(r.code,0,r.err);assert.equal(r.err,'');
assert.equal(r.out,['plan: activate for acme',`from: ${B}`,`to: ${C}`,
'store schema: unknown (the bus stopped during the read, so the store state changed; run --plan again)','target schema: 3h',
'commits: the run adds 2 (newest first, at most 200):',...commitLines(`${B}..${C}`),...planTail(e,C),''].join('\n'));
assert.deepEqual(fp(e),before);assert.equal(logLines(e).length,logs);
});
await t.test('rollback --restore',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
const stamp=readFileSync(join(e.data,'backups','stack',act,'STAMP'),'utf8').replace(/\n+$/,'');
fault(e,'bus-stops-after','1');const before=fp(e),n=calls(e).length;
const r=await run(e,'rollback','acme','--restore',act);assert.equal(r.code,3,r.out+r.err);
assert.equal(r.err,`stack-release: refusing restore-mismatch: the bus stopped while the events after ${stamp} were counted, so the store state changed; nothing was stopped\n`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(posts(e).length,0);
assert.equal(logLines(e).at(-1).code,'restore-mismatch');
});
});
test('T30 a copy check that agrees on another copy fails the gate with copy-check and rolls back',async(t)=>{
const e=await mk(t);await seed(e);
// A repo of its own, so the stand-in check replaces stack-copy.sh for this test only. Sourced, the
// stand-in is the real script; run with `check`, it reports all three on the pointer's copy.
const own=join(e.root,'repo');cpSync(repo,own,{recursive:true});
const sc=join(own,'scripts','stack-copy.sh'),at=join(COPIES,B);cpSync(sc,join(own,'scripts','stack-copy.real.sh'));
writeFileSync(sc,`#!/usr/bin/env bash\nreal="$(dirname "\${BASH_SOURCE[0]}")/stack-copy.real.sh"\nif [ "\${BASH_SOURCE[0]}" != "$0" ]; then source "$real"; return 0; fi\nif [ "\${1:-}" = check ]; then printf 'bus: %s\\nConsole: %s\\nCLI: %s\\n' '${at}' '${at}' '${at}'; exit 0; fi\nexec bash "$real" "$@"\n`);
e.sr=join(own,'scripts','stack-release.sh');
const r=await run(e,'activate',C1,'acme','--timeout','3');assert.equal(r.code,1,r.out+r.err);
assert.equal(r.err,rolledBack('copy-check'));
assert.ok(r.out.includes(`stack-release: gate failed on ${C1}: copy-check (the bus, Console and CLI agree on ${at}, not ${join(COPIES,C1)})\n`),r.out);
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.result,l.code,l.rollback],['activate','gate-failed','copy-check','ok']);
assert.equal(pointer(e).copy,B);assert.deepEqual([units(e)[BUS].state,units(e)[BUS].wd],['active',at]);
});
test('T31 a quoted or escaped Environment= or ExecStart= is unreadable: refusals by name, nothing read from /a',async(t)=>{
const forms=[
['Environment double quotes',(e)=>edit(join(e.units,'[email protected]'),`Environment=MOSAIC_CLAUDE_BIN=${e.claude}\n`,'Environment="MOSAIC_CLAUDE_BIN=/a b/claude"\n')],
['Environment single quotes',(e)=>edit(join(e.units,'[email protected]'),`Environment=MOSAIC_CLAUDE_BIN=${e.claude}\n`,'Environment=\'MOSAIC_CLAUDE_BIN=/a b/claude\'\n')],
['Environment backslash',(e)=>edit(join(e.units,'[email protected]'),`Environment=MOSAIC_CLAUDE_BIN=${e.claude}\n`,'Environment=MOSAIC_CLAUDE_BIN=/a\\ b/claude\n')],
['ExecStart quotes',(e)=>edit(join(e.units,'[email protected]'),' bus start %i\n',' bus start %i "--pm"\n')],
['Console ExecStart quotes',(e)=>edit(join(e.units,CON),' --business acme\n',' --business "acme"\n')],
];
for(const [name,change] of forms)await t.test(name,async(t)=>{
const e=await mk(t);await seed(e);change(e);const before=fp(e),n=calls(e).length;
const r=await run(e,'activate',C1,'acme');assert.equal(r.code,3,r.out+r.err);
assert.equal(r.err,`stack-release: refusing unit-flags-drift: cannot read ${BUS} or ${CON}\n`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
const s=await run(e,'status','acme');assert.equal(s.code,0,s.err);assert.ok(s.out.includes('unit flags: cannot read the units\n'),s.out);
const a=await mk(t);await seed(a,{pointer:'none'});change(a);
const d=await run(a,'adopt','acme');assert.equal(d.code,3,d.out+d.err);
assert.equal(d.err,`stack-release: refusing check-failed: cannot read ${BUS} or ${CON}\n`);assert.equal(pointer(a),null);
for(const x of [r,d])assert.ok(!x.err.includes('/a'),x.err);
});
});
// --- Row 169 (#1666): docs/plans/2026-10-19_pm-after-harness.md items 1 to 4 ---
const HARNESS={harness:['claude-code']};
const busUnit=(e)=>readFileSync(join(e.units,'[email protected]'),'utf8');
const execLine=(to,pm)=>`ExecStart=${COPIES}/${to}/scripts/mosaic bus start %i${pm?' --pm':''}\n`;
const sinceLines=(e)=>lines(join(e.sd,'journal-since.log')).length;
const PENDING_MSG='the pointer is pending (activate --pm-after-harness); run pm-on or rollback first';
// The stack after phase 1: seeded on B with --pm, then activate C1 --pm-after-harness.
async function pending(e){
await seed(e,{pm:true});
const r=await run(e,'activate',C1,'acme','--pm-after-harness');assert.equal(r.code,0,r.out+r.err);
}
// A refusal before anything stops: exit 3, the message, no file changed, no start or stop, one log line.
async function refused(e,args,code,msg){
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
const r=await run(e,...args);assert.equal(r.code,3,r.out+r.err);
assert.equal(r.err,`stack-release: refusing ${code}: ${msg}\n`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
assert.equal(logLines(e).length,logs+1);assert.deepEqual([logLines(e).at(-1).result,logLines(e).at(-1).code],['refused',code]);
}
test('T32 item 1: --pm-after-harness refuses a target without harness or a file that sets it, and passes without the PM',async(t)=>{
await t.test('usage: not with --pm, and only for activate',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});const logs=logLines(e).length;
for(const args of [['activate',C1,'acme','--pm','--pm-after-harness'],['activate',C1,'acme','--pm-after-harness','--pm'],['rollback','acme','--pm-after-harness'],['pm-on','acme','--pm-after-harness']]){
const r=await run(e,...args);assert.equal(r.code,4,args.join(' '));assert.match(r.err,/^usage: stack-release.sh adopt BUSINESS\n/);
}
assert.equal(logLines(e).length,logs);
});
await t.test('pm-after-harness-target',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});
await refused(e,['activate',A,'acme','--pm-after-harness'],'pm-after-harness-target',`${A} has no harness plug-in kind, so there is nothing to wait for; use --pm`);
});
await t.test('pm-after-harness-set',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});
for(const [h,shown] of [[['claude-code'],'["claude-code"]'],[[],'[]']]){
biz(e,{harness:h});
await refused(e,['activate',C1,'acme','--pm-after-harness'],'pm-after-harness-set',`the business file already sets harness ${shown}; use --pm`);
}
});
await t.test('a pointer without pm',async(t)=>{
const e=await mk(t);await seed(e);
await refused(e,['activate',C1,'acme','--pm-after-harness'],'unit-flags-drift','the units differ from the pointer; settle it first:\n pm: pointer 0, --pm-after-harness needs a pointer with pm 1');
});
await t.test('phase 1 installs without --pm, skips pm_launched and writes the pending pointer',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});const since=sinceLines(e),n=calls(e).length;
const r=await run(e,'activate',C1,'acme','--pm-after-harness');assert.equal(r.code,0,r.out+r.err);
assert.ok(r.out.endsWith(`stack-release: activate ok: ${C1} is live for acme without its PM; the PM is pending: Jason's plugins set of harness, then pm-on\n`),r.out);
assert.ok(busUnit(e).includes(execLine(C1,false)),busUnit(e));
assert.equal(sinceLines(e),since);
assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]);
const {at,activation,...p}=pointer(e);
assert.deepEqual(p,{copy:C1,business:'acme',pm:false,pmPending:true,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.from,l.to,l.result,l.pm],['activate',B,C1,'ok','pending']);
});
});
test('T33 item 2: read_pointer takes pmPending only as true beside pm false; a pending pointer refuses activate and shows in status',async(t)=>{
await t.test('read_pointer',async(t)=>{
const e=await mk(t);await seed(e);const good=pointer(e);
for(const bad of [{pmPending:false},{pmPending:'true'},{pmPending:1},{pmPending:null},{pm:true,pmPending:true}]){
writeFileSync(e.pointer,JSON.stringify({...good,...bad})+'\n');
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
for(const args of [['activate',C1,'acme'],['status','acme']]){
const r=await run(e,...args);assert.equal(r.code,1,`${JSON.stringify(bad)} ${args[0]}`);
assert.ok(r.err.endsWith(`stack-release: ${e.pointer} is unreadable; Jason or Mos settles it\n`),r.err);
}
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
}
});
await t.test('pm-pending, already-adopted and the status line',async(t)=>{
const e=await mk(t);await pending(e);
for(const args of [['activate',C,'acme','--migrate'],['activate',C,'acme','--pm','--migrate'],['activate',C1,'acme','--pm-after-harness']])await refused(e,args,'pm-pending',PENDING_MSG);
await refused(e,['adopt','acme'],'already-adopted',`${e.pointer} exists (copy ${C1})`);
const before=tree(e.data),r=await run(e,'status','acme');assert.equal(r.code,0,r.err);
const out=r.out.split('\n');
assert.deepEqual(out.slice(0,3),[`pointer: ${e.pointer}`,` ${readFileSync(e.pointer,'utf8').trimEnd()}`,'pm: pending (activate --pm-after-harness); next: Jason\'s plugins set of harness, then stack-release.sh pm-on acme']);
assert.ok(out.includes('unit flags: match the pointer'),r.out);
assert.deepEqual(tree(e.data),before);
});
});
test('T34 item 3: pm-on refuses before any stop, passes with the PM, or puts the phase-1 bus back',async(t)=>{
await t.test('not-pending',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});biz(e,HARNESS);
await refused(e,['pm-on','acme'],'not-pending','the pointer is not pending; pm-on follows activate --pm-after-harness');
});
await t.test('harness-unset',async(t)=>{
const e=await mk(t);await pending(e);
await refused(e,['pm-on','acme'],'harness-unset','the business file sets no harness; Jason sets it with plugins set first');
biz(e,{harness:[]});
await refused(e,['pm-on','acme'],'harness-unset','the business file\'s harness is [], not a non-empty list');
});
await t.test('a launched line passes: the bus alone restarts with --pm',async(t)=>{
const e=await mk(t);await pending(e);biz(e,HARNESS);
const since=sinceLines(e),n=calls(e).length,con=sha(join(e.units,CON)),act=pointer(e).activation;
const r=await run(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
assert.ok(r.out.endsWith(`stack-release: pm-on ok: the PM is live on ${C1} for acme\n`),r.out);
assert.deepEqual(startsStops(e,n),[`stop ${BUS}`,`start ${BUS}`]);
assert.ok(busUnit(e).includes(execLine(C1,true)),busUnit(e));assert.equal(sha(join(e.units,CON)),con);
assert.equal(sinceLines(e),since+1);assert.match(lines(join(e.sd,'journal-since.log')).at(-1),/^@\d+$/);
const {at,activation,...p}=pointer(e);assert.notEqual(activation,act);
assert.deepEqual(p,{copy:C1,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.to,l.result,l.pm,l.activation],['pm-on',C1,'ok','on',activation]);
const s=await run(e,'status','acme');assert.equal(s.code,0,s.err);assert.ok(!s.out.includes('pm: pending'),s.out);
});
await t.test('a failed pm_launched puts the phase-1 unit back: exit 1, still pending',async(t)=>{
const e=await mk(t);await pending(e);biz(e,HARNESS);fault(e,`pm-${C1}`,'none');
const before=fp(e),p0=readFileSync(e.pointer,'utf8'),n=calls(e).length;
const r=await run(e,'pm-on','acme','--timeout','3');assert.equal(r.code,1,r.out+r.err);
const l=logLines(e).at(-1),rs=join(e.state,'stack-rollback',l.activation);
assert.equal(r.err,`stack-release: pm-on failed with pm-launch-unconfirmed (no launched line since the start); the bus runs ${C1} without its PM again and passed the phase-1 gate, and the pointer stays pending. Fix harness with plugins set, then run pm-on again. Rollback set: ${rs}\n`);
assert.deepEqual([l.op,l.result,l.code,l.why],['pm-on','pm-on-failed','pm-launch-unconfirmed','no launched line since the start']);
assert.deepEqual(fp(e),before);assert.ok(busUnit(e).includes(execLine(C1,false)));
assert.equal(readFileSync(e.pointer,'utf8'),p0);
assert.deepEqual([units(e)[BUS].state,units(e)[BUS].wd,units(e)[CON].state],['active',join(COPIES,C1),'active']);
assert.deepEqual(startsStops(e,n),[`stop ${BUS}`,`start ${BUS}`,`stop ${BUS}`,`start ${BUS}`]);
});
await t.test('a failed phase-1 gate too: pm-on-down, exit 2, both units stopped',async(t)=>{
const e=await mk(t);await pending(e);biz(e,HARNESS);fault(e,`healthz-${C1}`);
const p0=readFileSync(e.pointer,'utf8'),store=sha(e.store);
const r=await run(e,'pm-on','acme','--timeout','3');assert.equal(r.code,2,r.out+r.err);
const l=logLines(e).at(-1),rs=join(e.state,'stack-rollback',l.activation);
assert.equal(r.err,`stack-release: pm-on-down: pm-on failed with healthz and the phase-1 bus with healthz; both units are stopped. Rollback set: ${rs}\n`);
assert.deepEqual([l.result,l.code,l.phase1],['pm-on-down','healthz','healthz']);
assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['inactive','inactive']);
assert.equal(readFileSync(e.pointer,'utf8'),p0);assert.equal(sha(e.store),store);
assert.ok(busUnit(e).includes(execLine(C1,false)));
});
});
test('T35 item 4: rollback from pending installs --pm; a plug-in kind the target lacks refuses by name',async(t)=>{
await t.test('rollback from pending gates pm_launched on the old copy',async(t)=>{
const e=await mk(t);await pending(e);const since=sinceLines(e);
const r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
assert.ok(busUnit(e).includes(execLine(B,true)),busUnit(e));
assert.equal(sinceLines(e),since+1);
const {at,activation,...p}=pointer(e);
assert.deepEqual(p,{copy:B,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
});
await t.test('after the harness set, rollback refuses until harness is gone',async(t)=>{
const e=await mk(t);await pending(e);biz(e,HARNESS);
assert.equal((await run(e,'pm-on','acme')).code,0);
await refused(e,['rollback','acme'],'unknown-kind-for-target',`the business file names plug-in kind(s) harness, which ${B} does not know; set each to null with plugins set through the live bus first`);
biz(e);
const r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
assert.ok(busUnit(e).includes(execLine(B,true)));assert.deepEqual([pointer(e).copy,pointer(e).pm],[B,true]);
});
await t.test('activate names every kind the target lacks',async(t)=>{
const e=await mk(t);await seed(e);
biz(e,{...HARNESS,widget:{}});
await refused(e,['activate',A,'acme'],'unknown-kind-for-target',`the business file names plug-in kind(s) harness widget, which ${A} does not know`);
await refused(e,['activate',C1,'acme'],'unknown-kind-for-target',`the business file names plug-in kind(s) widget, which ${C1} does not know`);
});
});
test('T36 --plan shows the pending state and lists the row 169 refusals it can see',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});
const quiet=async(code,...args)=>{
const before=fp(e),n=calls(e).length,logs=logLines(e).length,r=await run(e,...args);
assert.equal(r.code,code,r.out+r.err);assert.equal(r.err,'');
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
return r.out.split('\n');
};
let out=await quiet(0,'activate',C1,'acme','--pm-after-harness','--plan');
assert.deepEqual(out.slice(-5),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
biz(e,HARNESS);
out=await quiet(3,'activate',A,'acme','--pm-after-harness','--plan');
assert.deepEqual(out.slice(-6),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
out=await quiet(3,'activate',C1,'acme','--pm-after-harness','--plan');
assert.equal(out.at(-2),'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm');
biz(e);assert.equal((await run(e,'activate',C1,'acme','--pm-after-harness')).code,0);
out=await quiet(3,'activate',C,'acme','--plan');
assert.deepEqual(out.slice(-6),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
out=await quiet(0,'rollback','acme','--plan');
assert.deepEqual(out.slice(-5),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
});
// Row 176 test 11: supported and older Console copies deliberately differ from the checkout.
const testerFlags=(e,port='7333')=>['--port',e.port,'--business','acme','--tester-port',port];
const consoleText=(e)=>readFileSync(join(e.units,CON),'utf8');
const stepText=(e)=>readFileSync(join(e.state,'stack-rollback',pointer(e).activation,'steps.log'),'utf8');
test('T37 tester-port activate writes pointer and unit, replaces the port, and supported re-pin and rollback keep it',async(t)=>{
const e=await mk(t);await seed(e);
let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);
r=await run(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
r=await run(e,'activate',B,'acme','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));
assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);assert.doesNotMatch(consoleText(e),/--tester-port 7333/);
assert.equal((consoleText(e).match(/--tester-port/g)||[]).length,1);
r=await run(e,'activate',C1,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/Console: .*--tester-port 7334/);
r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));
r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,B);
assert.deepEqual(pointer(e).console,testerFlags(e,'7334'));assert.match(consoleText(e),/--tester-port 7334(?:\n|$)/);
});
test('T38 tester-port rollback to unsupported target drops flag in plan, steps, unit and pointer',async(t)=>{
const e=await mk(t);await seed(e,{copy:A});
let r=await run(e,'activate',B,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);
const before=fp(e),n=calls(e).length;
r=await run(e,'rollback','acme','--plan');assert.equal(r.code,0,r.out+r.err);
assert.match(r.out,/drop[^\n]*--tester-port 7333/i);assert.doesNotMatch(r.out.split('\n').find((l)=>l.startsWith('Console: ')),/--tester-port/);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,A);
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);
assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);
});
test('T39 tester-port explicit unsupported target reads target copy and refuses before stopping either unit',async(t)=>{
const e=await mk(t);await seed(e);const before=fp(e),n=calls(e).length;
const r=await run(e,'activate',A,'acme','--tester-port','7333');assert.equal(r.code,3,r.out+r.err);
assert.match(r.err,/refusing tester-port-target:/);assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
assert.equal(units(e)[BUS].state,'active');assert.equal(units(e)[CON].state,'active');
// The repository's Console accepts the flag while A's Console does not: probing REPO fails this test.
assert.match(readFileSync(join(repo,'packages/webui/src/cli.mjs'),'utf8'),/--tester-port/);
});
test('T40 tester-port invalid values refuse exit 4 before stopping, including tunnel and main ports',async(t)=>{
const e=await mk(t);await seed(e);const before=fp(e),n=calls(e).length;
for(const value of ['7332',e.port,'1023','65536','x','7333.5']){
const r=await run(e,'activate',C1,'acme','--tester-port',value);assert.equal(r.code,4,`${value}: ${r.out}${r.err}`);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
}
});
test('T41 tester-port inherited activate to unsupported target drops it and retains drift refusal and failed-run recovery',async(t)=>{
const e=await mk(t);await seed(e);
let r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);
r=await run(e,'activate',A,'acme','--plan');assert.equal(r.code,0,r.out+r.err);assert.match(r.out,/drop[^\n]*--tester-port 7333/i);
fault(e,`check-${A}`);const before=fp(e);
r=await run(e,'activate',A,'acme','--timeout','3');assert.equal(r.code,1,r.out+r.err);assert.deepEqual(fp(e),before);
rmSync(join(e.sd,'faults',`check-${A}`));
edit(join(e.units,CON),'--tester-port 7333','--tester-port 7334');const n=calls(e).length;
r=await run(e,'activate',A,'acme');assert.equal(r.code,3,r.out+r.err);assert.match(r.err,/unit-flags-drift/);assert.deepEqual(startsStops(e,n),[]);
edit(join(e.units,CON),'--tester-port 7334','--tester-port 7333');
r=await run(e,'activate',A,'acme');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme']);assert.doesNotMatch(consoleText(e),/--tester-port/);
assert.match(stepText(e),/drop[^\n]*--tester-port 7333/i);
});
test('T42 tester-port pm-on preserves pointer Console flags',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});
let r=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);
biz(e,HARNESS);
r=await run(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,testerFlags(e));assert.match(consoleText(e),/--tester-port 7333(?:\n|$)/);
});
// Row 180 item 1 (P19 391): a date stand-in prints one second for every call that names a time,
// so each run below starts in that second, as two runs within one real second do. Calls for
// +%s (the gate's start mark) pass through, so each gate still reads only its own start.
function pinClock(e){
const dir=join(e.root,'clock'),second=Math.floor(Date.now()/1000);mkdirSync(dir);
writeFileSync(join(dir,'date'),`#!/bin/sh\ncase "$*" in *%s*) exec /usr/bin/date "$@" ;; esac\nexec /usr/bin/date -d @${second} "$@"\n`,{mode:0o755});
e.env={...e.env,PATH:`${dir}:${e.env.PATH}`};
return execFileSync(join(dir,'date'),['-u','+%Y%m%dT%H%M%SZ'],{env:e.env,encoding:'utf8'}).trim();
}
const rollbackSets=(e)=>readdirSync(join(e.state,'stack-rollback')).sort();
test('T43 row 180 item 1: runs in one second get distinct activation ids, rollback sets and post-attempt stores',async(t)=>{
await t.test('activate then pm-on on the same copy',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});const sec=pinClock(e),id=`${sec}-${C1.slice(0,12)}`;
let r=await run(e,'activate',C1,'acme','--pm-after-harness');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).activation,id);
biz(e,HARNESS);
r=await run(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).activation,`${id}-1`);
assert.deepEqual(logLines(e).slice(-4).map((l)=>[l.op,l.result,l.activation]),[['activate','started',id],['activate','ok',id],['pm-on','started',`${id}-1`],['pm-on','ok',`${id}-1`]]);
assert.deepEqual(rollbackSets(e),[id,`${id}-1`]);
for(const a of [id,`${id}-1`])assert.ok(existsSync(join(e.state,'stack-rollback',a,'SHA256SUMS')),a);
});
await t.test('two restores: an old-form id and a suffixed one, each with its own post-attempt store',async(t)=>{
const e=await mk(t);await seed(e);const sec=pinClock(e),c=`${sec}-${C.slice(0,12)}`,b=`${sec}-${B.slice(0,12)}`;
for(const [act,restore] of [[c,b],[`${c}-1`,`${b}-1`]]){
let r=await run(e,'activate',C,'acme','--migrate');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).activation,act);
assert.ok(existsSync(join(e.data,'backups','stack',act,'SHA256SUMS')),act);
r=await run(e,'rollback','acme','--restore',act);assert.equal(r.code,0,r.out+r.err);
const post=join(e.data,`bus.post-attempt-${sec}${restore.slice(b.length)}`);
assert.ok(r.out.startsWith(`restore: ${join(e.data,'backups','stack',act)}; the store now in place moves to ${post} and is kept; 0 event(s) were written after the backup\n`),r.out);
assert.deepEqual([pointer(e).copy,pointer(e).activation,pointer(e).schema],[B,restore,'3g']);
assert.equal(logLines(e).at(-1).backup,act);
}
assert.deepEqual(posts(e).sort(),[`bus.post-attempt-${sec}`,`bus.post-attempt-${sec}-1`]);
assert.deepEqual(readdirSync(join(e.data,'backups','stack')).sort(),[c,`${c}-1`]);
assert.deepEqual(rollbackSets(e),[c,`${c}-1`,b,`${b}-1`].sort());
});
await t.test('adopt takes the next free id too',async(t)=>{
const e=await mk(t);await seed(e,{pointer:'none'});const id=`${pinClock(e)}-${B.slice(0,12)}`;
mkdirSync(join(e.state,'stack-rollback',id),{recursive:true});
const r=await run(e,'adopt','acme');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).activation,`${id}-1`);
assert.deepEqual(logLines(e).map((l)=>[l.op,l.result,l.activation]),[['adopt','ok',`${id}-1`]]);
});
await t.test('all ten names taken: capture-failed before anything stops, and the log line names no activation',async(t)=>{
const e=await mk(t);await seed(e);const sec=pinClock(e),id=`${sec}-${C1.slice(0,12)}`;
// A rollback set, a backup or a post-attempt store each takes a name.
for(const s of ['','-1','-2','-3','-4','-5','-6','-7'])mkdirSync(join(e.state,'stack-rollback',`${id}${s}`),{recursive:true});
mkdirSync(join(e.data,'backups','stack',`${id}-8`),{recursive:true});writeFileSync(join(e.data,`bus.post-attempt-${sec}-9`),'');
await refused(e,['activate',C1,'acme'],'capture-failed',`every activation id for ${id} is taken; nothing was stopped`);
assert.equal(logLines(e).at(-1).activation,null);
});
await t.test('--restore takes an id with or without -1 to -9 and nothing else',async(t)=>{
const e=await mk(t);await seed(e);const id=`20261020T013708Z-${B.slice(0,12)}`;
for(const bad of [`${id}-0`,`${id}-10`,`${id}-a`,`${id}-`,`${id}1`]){
const r=await run(e,'rollback','acme','--restore',bad,'--plan');assert.equal(r.code,4,bad);
}
for(const good of [id,`${id}-1`,`${id}-9`]){
const r=await run(e,'rollback','acme','--restore',good,'--plan');assert.notEqual(r.code,4,`${good}: ${r.err}`);
}
});
});
test('T44 row 180 item 2: --plan lists tester-port-invalid and tester-port-target with its other refusals',async(t)=>{
const usageText=async(e)=>(await run(e,'--help')).err;
await t.test('a bad tester port',async(t)=>{
const e=await mk(t);await seed(e,{pm:true});biz(e,HARNESS);
const before=fp(e),n=calls(e).length,logs=logLines(e).length,p0=readFileSync(e.pointer,'utf8');
const rule=(port,main)=>`--tester-port ${port} is not a whole port from 1024 to 65535 other than 7332${main?` and the Console's --port ${main}`:''}`;
// The fixture's Console has --port e.port; a pointer without --port has the default 7332, which
// the reason names once. --plan reads the pointer's flags and not the units, so only it changes.
const noPort=()=>{const q=JSON.parse(p0);q.console=q.console.filter((f,i,a)=>f!=='--port'&&a[i-1]!=='--port');writeFileSync(e.pointer,JSON.stringify(q)+'\n')};
for(const [port,reason,edit] of [['80',rule('80',e.port)],[e.port,rule(e.port,e.port)],['80',rule('80'),noPort],['7332',rule('7332'),noPort]]){
if(edit)edit();
const r=await run(e,'activate',C1,'acme','--pm-after-harness','--plan','--tester-port',port);
assert.equal(r.code,3,r.out+r.err);assert.equal(r.err,'');
assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: ${reason}`,'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm']);
const u=await run(e,'activate',C1,'acme','--pm-after-harness','--tester-port',port);
assert.equal(u.code,4,u.out+u.err);assert.equal(u.out,'');assert.equal(u.err,await usageText(e));
writeFileSync(e.pointer,p0);
}
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
});
await t.test('without --plan a bad port is usage before any pointer read',async(t)=>{
// parse_opts checks it, so a host with no pointer gets usage and not no-pointer (exit 3, a log line).
const e=await mk(t);await seed(e,{pointer:'none'});
const n=calls(e).length,u=await run(e,'activate',C1,'acme','--tester-port','80');
assert.equal(u.code,4,u.out+u.err);assert.equal(u.out,'');assert.equal(u.err,await usageText(e));
assert.deepEqual(logLines(e),[]);assert.deepEqual(startsStops(e,n),[]);
});
await t.test('a target Console without --tester-port',async(t)=>{
const e=await mk(t);await seed(e);biz(e,HARNESS);
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
const r=await run(e,'activate',A,'acme','--tester-port','7333','--plan');
assert.equal(r.code,3,r.out+r.err);assert.equal(r.err,'');
assert.deepEqual(r.out.split('\n').filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-target: ${A}'s Console does not support --tester-port`,`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
await refused(e,['activate',A,'acme','--tester-port','7333'],'tester-port-target',`${A}'s Console does not support --tester-port`);
});
});
// Row 181 (#1679, P19 390): COUNTED and STORE_JS as the script under test has them. The fixture
// tables carry only seq and at, which is all the count reads.
const srText=readFileSync(SR,'utf8');
const COUNTED=/^COUNTED="([^"]*)"$/m.exec(srText)[1].split(' ');
const STORE_JS=/^STORE_JS='([\s\S]*?)'$/m.exec(srText)[1];
// sage-1163's list 2, written out so that a table dropped from the script's COUNTED fails T45 as
// well as T46.
const LISTED=['role_claims','thread_claims','decision_events','messages','deliveries','task_snapshots','projects','threads','thread_opens','instance_names','usage_records'];
const LATE='2999-01-01T00:00:00.000Z';
const addRow=(file,table,at=LATE)=>{const db=new DatabaseSync(file);db.exec(`CREATE TABLE IF NOT EXISTS ${table} (seq INTEGER PRIMARY KEY, at TEXT NOT NULL)`);db.prepare(`INSERT INTO ${table} (at) VALUES (?)`).run(at);db.close()};
test('T45 row 181 items 2 and 3: the restore guard counts events and every COUNTED table',async(t)=>{
await t.test('both modes count events and each listed table, and a table the store lacks counts 0',async(t)=>{
const d=mkdtempSync(join(tmpdir(),'sr181-'));t.after(()=>rmSync(d,{recursive:true,force:true}));
const stamp='2020-01-01T00:00:00.000Z',count=(f,mode)=>execFileSync(NODE,['-e',STORE_JS,'--',mode,f,stamp,COUNTED.join(' ')],{encoding:'utf8'});
const store=(name,rows,tables=rows)=>{
const f=join(d,name);for(const x of ['events',...tables])addRow(f,x,'2000-01-01T00:00:00.000Z');for(const x of rows)addRow(f,x);return f;
};
const both=(f,want)=>{const s=sha(f);for(const mode of ['count','heldcount'])assert.equal(count(f,mode),want,`${basename(f)} ${mode}`);assert.equal(sha(f),s);assert.deepEqual(readdirSync(d).filter((n)=>n.startsWith(`${basename(f)}-`)),[])};
for(const x of LISTED)both(store(`${x}.sqlite`,['events',x]),`2\t1 event(s), 1 ${x} row(s)\n`);
both(store('brief.sqlite',['events','usage_records','instance_names']),'3\t1 event(s), 1 instance_names row(s), 1 usage_records row(s)\n');
// A 3f store: no usage_records table, and nothing to refuse.
both(store('3f.sqlite',['events'],['events','instance_names']),'1\t1 event(s)\n');
both(store('none.sqlite',[],['instance_names']),'0\t0 event(s)\n');
});
for(const x of ['instance_names','usage_records'])await t.test(`one ${x} row and no event refuses before any stop; --events 1 restores`,async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
addRow(e.store,x);
const want=`stack-release: refusing post-backup-events: 0 event(s), 1 ${x} row(s) were written after the backup; Jason decides, then --events 1\n`;
for(const extra of [[],['--events','2']]){
const before=fp(e),n=calls(e).length,r=await run(e,'rollback','acme','--restore',act,...extra);
assert.equal(r.code,3,extra.join(' '));assert.equal(r.err,want);assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
}
const r=await run(e,'rollback','acme','--restore',act,'--events','1');assert.equal(r.code,0,r.out+r.err);
assert.ok(r.out.includes(`; 0 event(s), 1 ${x} row(s) were written after the backup\n`),r.out);
assert.deepEqual([pointer(e).copy,pointer(e).schema],[B,'3g']);assert.equal(sha(e.store),backupSum(e,act));
});
await t.test('a listed table that cannot be read refuses restore-mismatch before any stop',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
const stamp=readFileSync(join(e.data,'backups','stack',act,'STAMP'),'utf8').replace(/\n+$/,'');
const db=new DatabaseSync(e.store);db.exec('CREATE TABLE usage_records (seq INTEGER PRIMARY KEY)');db.close();
const before=fp(e),n=calls(e).length,r=await run(e,'rollback','acme','--restore',act);
assert.equal(r.code,3,r.out+r.err);assert.ok(r.err.endsWith(`stack-release: refusing restore-mismatch: cannot count the rows after ${stamp}\n`),r.err);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).at(-1).code,'restore-mismatch');
});
await t.test('the in-run recount counts the listed tables too',async(t)=>{
const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
fault(e,'stop-writes-row','thread_opens');
const r=await run(e,'rollback','acme','--restore',act);assert.equal(r.code,1,r.out+r.err);
assert.equal(r.err,rolledBack('post-backup-events',C));
assert.ok(r.out.includes('; 0 event(s) were written after the backup\n'),r.out);
assert.deepEqual([pointer(e).copy,pointer(e).schema],[C,'3h']);assert.equal(posts(e).length,0);
const l=logLines(e).at(-1);assert.deepEqual([l.result,l.code,l.rollback],['gate-failed','post-backup-events','ok']);
});
});
test('T46 row 181 item 4: every table in the bus schema is in exactly one of four lists',()=>{
// The schema a target copy's store gets. Lists 1 and 2 come from the script, so a table added to
// COUNTED or dropped from it shows here. Row 181's BUILD.md names each writer.
const db=new DatabaseSync(':memory:');db.exec(readFileSync(join(ROOT,'packages','bus','schema.sql'),'utf8'));
const tables=db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map((r)=>r.name);
const hasAt=(n)=>db.prepare(`SELECT count(*) AS n FROM pragma_table_info(?) WHERE name='at'`).get(n).n===1;
const always={decisions:'decision.raise writes action.allowed in the same transaction'};
const untimed={
meta:'key and value only; its one UPDATE is the migration step',
message_threads:'no time column; each row goes with a messages row, which is counted',
sqlite_sequence:'SQLite\'s own AUTOINCREMENT table',
task_comment_cursors:'no time column; a restore drops its advances uncounted (BUILD.md)',
};
const lists=[['events'],COUNTED,Object.keys(always),Object.keys(untimed)],all=lists.flat();
assert.equal(new Set(all).size,all.length,`a table is in two lists: ${all.filter((n,i)=>all.indexOf(n)!==i)}`);
assert.deepEqual(tables.filter((n)=>!all.includes(n)),[],'tables in no list');
assert.deepEqual(all.filter((n)=>!tables.includes(n)),[],'listed tables the schema lacks');
for(const n of lists.slice(0,3).flat())assert.ok(hasAt(n),`${n} has at`);
for(const n of lists[3])assert.ok(!hasAt(n),`${n} has no at`);
db.close();
});
// Row 178: proof metadata uses temp fixture files, never the installed slot.
async function seedProof(e) {
await seed(e);
const file=join(e.root,'proof.fixture');writeFileSync(file,'fixture_proxy_proof_012345678901234567890123456789',{mode:0o600});
const extra=['--public-host','console.example','--auth-header','x-user','--allow-user','jason','--proof-header','x-console-proof','--proof-file',file];
const p=pointer(e);p.console.push(...extra);writeFileSync(e.pointer,JSON.stringify(p)+'\n');
const unit=join(e.units,CON);edit(unit,` --business acme\n`,` --business acme ${extra.join(' ')}\n`);
return p.console;
}
test('T178 proxy proof header and path survive re-pin and rollback unchanged in the pointer and unit',async t=>{
const e=await mk(t),expected=await seedProof(e);
let r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,expected);assert.ok(consoleText(e).includes(expected.join(' ')));
r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,expected);assert.ok(consoleText(e).includes(expected.join(' ')));
});
test('T178 proxy proof pair survives a tester-port replacement as well',async t=>{
const e=await mk(t),expected=await seedProof(e);
const r=await run(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);
assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));
});