activate --send turns send on only when the run's Console flags hold --sso, given or carried (F39 b), and puts it after --sso and before the tester flags. activate --no-sso turns --sso off with the Console flags from the set of the activation that turned it on, drops --send (F39 a) and keeps the pointer's tester. Both are usage errors under --units external. T58 covers the brief's T1-T4; the external test covers T5. Design and the set-walk choice: agents/darkwing/work/queue-273/BUILD.md.
477 lines
31 KiB
JavaScript
477 lines
31 KiB
JavaScript
// scripts/stack-release.sh --units external (row 232, #1734). Design: r6 section 4 and the section 7
|
|
// test list, agents/darkwing/work/queue-195/DESIGN.md; deviations: agents/darkwing/work/queue-232/BUILD.md.
|
|
// The stand-ins and the module fixture are stack-release.test.mjs's (stack-release-fixture.mjs).
|
|
// Root's part (stop, unit install, start) runs here as the stand-in systemctl and bus-service.sh and
|
|
// webui-service.sh installs; no sudo and no real unit.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {execFileSync} from 'node:child_process';
|
|
import {mkdirSync,writeFileSync,readFileSync,existsSync,readdirSync,rmSync,chmodSync,cpSync} from 'node:fs';
|
|
import {DatabaseSync} from 'node:sqlite';
|
|
import {join} from 'node:path';
|
|
import {B,C,C1,COPIES,ROOT,SBIN,base,fault,lines,logLines,mk,pointer,repo,run,seed,sha,storeVersion,sysctl,tree,units} from './stack-release-fixture.mjs';
|
|
|
|
// The commit row 232 builds on: the user-mode tests compare the candidate's calls and --plan bytes
|
|
// with its scripts. A later row that changes user mode on purpose moves this.
|
|
const BASE='cb5fdb064fb4f12edd045374dd463dfdc25c2b42';
|
|
|
|
const X=(e)=>({...e,flags:[...e.flags,'--units','external']});
|
|
const sh=(e,name,...a)=>execFileSync('bash',[join(repo,'scripts',name),...a],{env:e.env,encoding:'utf8',stdio:['ignore','pipe','pipe']});
|
|
// Root's steps: R1/A2 stop, A4 install and start. offset is the start's SD_CLOCK_OFFSET.
|
|
const down=(e)=>{sysctl(e,'stop',e.con);sysctl(e,'stop',e.bus)};
|
|
const up=(e,copy,{pm=false,offset=0}={})=>{
|
|
sh(e,'bus-service.sh','install','--copy',copy,'--copies',COPIES,...(pm?['--pm']:[]),'--claude',e.claude);
|
|
sh(e,'webui-service.sh','install','--copy',copy,'--copies',COPIES,'--port',e.port,'--business','acme');
|
|
execFileSync(join(SBIN,'systemctl'),['--user','start',e.bus],{env:{...e.env,SD_CLOCK_OFFSET:String(offset)}});
|
|
sysctl(e,'start',e.con);
|
|
};
|
|
// The stack on B for acme, adopted under --units external, then stopped by root unless live.
|
|
async function xseed(t,{pm=false,live=false}={}){
|
|
const e=await mk(t);await seed(e,{pm,pointer:'none'});const x=X(e);
|
|
const r=await run(x,'adopt','acme');assert.equal(r.code,0,r.err);
|
|
if(!live)down(e);
|
|
return {e,x};
|
|
}
|
|
const lastAct=(e)=>logLines(e).filter((o)=>o.result==='started').at(-1).activation;
|
|
const rs=(e,a)=>join(e.state,'stack-rollback',a);
|
|
const open=(e)=>{const m=new Set();for(const o of logLines(e)){if(!o.activation)continue;if(o.result==='started')m.add(o.activation);else m.delete(o.activation)}return [...m]};
|
|
const mine=(e,a)=>logLines(e).filter((o)=>o.activation===a);
|
|
const journalCalls=(e)=>lines(join(e.sd,'journal-since.log'));
|
|
// A3: stop, stage TO. Returns the activation and the run.
|
|
async function stage(e,x,to,...args){
|
|
const r=await run(x,'activate',to,'acme',...args);
|
|
return {r,a:r.code===0||r.code===1?lastAct(e):null};
|
|
}
|
|
// The started line's from, to and op on the activation's result line, and its backup the set's.
|
|
function sameAsStarted(e,a){
|
|
const [s,f,...more]=mine(e,a);assert.equal(more.length,0,JSON.stringify(mine(e,a)));
|
|
assert.equal(s.result,'started');
|
|
for(const k of ['op','from','to','activation'])assert.equal(f[k],s[k],k);
|
|
assert.ok(['activate','rollback'].includes(f.op),f.op);
|
|
assert.equal(f.units,'external');
|
|
const b=join(rs(e,a),'BACKUP');assert.equal(f.backup,existsSync(b)?readFileSync(b,'utf8').trim():null);
|
|
return f;
|
|
}
|
|
// B1's end state: one started and one result line, nothing open, no marker or FAILED, and a
|
|
// following --plan that doesn't refuse interrupted-run.
|
|
async function closed(e,x,a){
|
|
sameAsStarted(e,a);
|
|
assert.deepEqual(open(e),[]);
|
|
for(const f of ['AWAITING-UNITS','FAILED'])assert.ok(!existsSync(join(rs(e,a),f)),f);
|
|
const s=await run(x,'status','acme');assert.equal(s.code,0,s.err);
|
|
assert.ok(!/interrupted run|awaiting units/.test(s.out),s.out);
|
|
const p=await run(x,'activate',pointer(e).copy===C1?B:C1,'acme','--plan');
|
|
assert.ok(!(p.out+p.err).includes('interrupted-run'),p.out+p.err);
|
|
}
|
|
const storeSha=(e)=>sha(e.store);
|
|
const backupStore=(e,a)=>join(e.data,'backups','stack',a,'bus','bus.sqlite');
|
|
|
|
test('row 232: confirm, revert, gate, --to and --expect-from need --units external; the user-only flags are usage under it, and so are row 273\'s --send and --no-sso',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const logs=logLines(e).length;
|
|
for(const a of [['confirm',C1,'acme'],['revert',C1,'acme'],['gate',B,'acme','--since','1'],['rollback','acme','--to',B],['activate',C1,'acme','--plan','--expect-from',B]]){
|
|
const r=await run(e,...a);assert.equal(r.code,4,a.join(' '));assert.match(r.err,/^usage: stack-release.sh adopt BUSINESS\n/);
|
|
}
|
|
for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--send'],['activate',C1,'acme','--no-sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){
|
|
const r=await run(x,...a);assert.equal(r.code,4,a.join(' '));
|
|
}
|
|
assert.equal(logLines(e).length,logs);
|
|
});
|
|
|
|
test('row 232: pm-on and activate --pm-after-harness refuse external-pm',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const p=await run(x,'pm-on','acme');assert.equal(p.code,3);
|
|
assert.match(p.err,/^stack-release: refusing external-pm: under --units external the PM is a flag in root's bus unit/);
|
|
const a=await run(x,'activate',C1,'acme','--pm-after-harness');assert.equal(a.code,3);
|
|
assert.match(a.err,/^stack-release: refusing external-pm: /);
|
|
const l=logLines(e).slice(-2);assert.deepEqual(l.map((o)=>[o.result,o.code,o.activation]),[['refused','external-pm',null],['refused','external-pm',null]]);
|
|
assert.equal(readdirSync(e.state).includes('stack-rollback'),false);
|
|
});
|
|
|
|
test('row 232: units-running (N4) refuses while a unit is active and leaves the store, the log and TAKEN; failed counts as down',async(t)=>{
|
|
const {e,x}=await xseed(t,{live:true});
|
|
sysctl(e,'stop',e.con);
|
|
const before=storeSha(e),logs=logLines(e).length;
|
|
const r=await run(x,'activate',C,'acme','--migrate');assert.equal(r.code,3,r.out+r.err);
|
|
assert.equal(r.err,`stack-release: refusing units-running: ${e.bus} is active; root stops the units first\n`);
|
|
assert.equal(storeSha(e),before);assert.equal(storeVersion(e.store,true),'3g');
|
|
const l=logLines(e).slice(logs);assert.deepEqual(l.map((o)=>[o.result,o.code,o.activation,o.units]),[['refused','units-running',null,'external']]);
|
|
assert.ok(!existsSync(join(e.data,'backups')));assert.ok(!existsSync(join(e.state,'stack-rollback')));
|
|
// The Console active and the bus stopped refuses too.
|
|
sysctl(e,'stop',e.bus);sysctl(e,'start',e.con);
|
|
const c=await run(x,'activate',C1,'acme');assert.equal(c.code,3);assert.ok(c.err.startsWith(`stack-release: refusing units-running: ${e.con} is active`),c.err);
|
|
// A bus unit root's stop left failed is down.
|
|
sysctl(e,'stop',e.con);
|
|
const s=units(e);s[e.bus].state='failed';writeFileSync(join(e.sd,'units.json'),JSON.stringify(s));
|
|
const f=await run(x,'activate',C1,'acme');assert.equal(f.code,0,f.out+f.err);
|
|
});
|
|
|
|
test('row 232: activate stages: the pointer stays, AWAITING-UNITS names TO, a backup of the stopped store, no directory in the set, no --user, no journal',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const p0=readFileSync(e.pointer,'utf8'),w0=sha(join(e.bin,'mosaic')),argv0=lines(join(e.sd,'argv.log')).length;
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
assert.equal(r.out.split('\n').at(-2),`stack-release: activate staged ${C1} for acme as ${a}; root installs and starts the units, then runs confirm ${C1} acme --units external`);
|
|
assert.equal(readFileSync(e.pointer,'utf8'),p0);
|
|
assert.equal(readFileSync(join(rs(e,a),'AWAITING-UNITS'),'utf8'),`${C1}\n`);
|
|
assert.deepEqual(readdirSync(rs(e,a)).sort(),['AWAITING-UNITS','BACKUP','SHA256SUMS','UNITS.sha256','WRAPPER_MODE','pointer',`show-${e.bus}.txt`,`show-${e.con}.txt`,'steps.log','wrapper'].sort());
|
|
assert.equal(readFileSync(join(rs(e,a),'BACKUP'),'utf8'),`${a}\n`);
|
|
assert.equal(sha(backupStore(e,a)),storeSha(e));assert.equal(readFileSync(join(e.data,'backups','stack',a,'SCHEMA'),'utf8'),'3g\n');
|
|
assert.equal(readFileSync(join(rs(e,a),'UNITS.sha256'),'utf8'),`${sha(join(e.units,e.tmpl))} ${join(e.units,e.tmpl)}\n${sha(join(e.units,e.con))} ${join(e.units,e.con)}\n`);
|
|
assert.equal(sha(join(rs(e,a),'wrapper')),w0);assert.notEqual(sha(join(e.bin,'mosaic')),w0);
|
|
assert.deepEqual(mine(e,a).map((o)=>o.result),['started']);
|
|
assert.deepEqual([units(e)[e.bus].state,units(e)[e.con].state],['inactive','inactive']);
|
|
const ext=lines(join(e.sd,'argv.log')).slice(argv0);
|
|
assert.ok(ext.length>0);for(const c of ext)assert.match(c,/^(show|is-active|list-units) /,c);
|
|
assert.deepEqual(journalCalls(e),[]);
|
|
const s=await run(x,'status','acme');assert.equal(s.code,0,s.err);
|
|
assert.ok(s.out.includes(`awaiting units: ${a} (copy ${C1})\n`),s.out);assert.ok(s.out.includes(`interrupted run: ${a} `),s.out);
|
|
});
|
|
|
|
test('row 232: A3 order: capture-failed and marker-failed refuse with exit 3 and leave no marker or FAILED',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
fault(e,'no-fragment');
|
|
const r=await run(x,'activate',C1,'acme');assert.equal(r.code,3,r.out+r.err);
|
|
assert.match(r.err,/^stack-release: refusing capture-failed: could not write the rollback set .*; nothing was changed\n$/);
|
|
const a=lastAct(e);
|
|
assert.deepEqual(mine(e,a).map((o)=>[o.result,o.code]),[['started',undefined],['refused','capture-failed']]);
|
|
for(const f of ['AWAITING-UNITS','FAILED'])assert.ok(!existsSync(join(rs(e,a),f)),f);
|
|
assert.deepEqual(open(e),[]);
|
|
rmSync(join(e.sd,'faults','no-fragment'));
|
|
// The set can't be made: marker-failed, for that activation.
|
|
chmodSync(join(e.state,'stack-rollback'),0o555);
|
|
const m=await run(x,'activate',C1,'acme');assert.equal(m.code,3,m.out+m.err);
|
|
assert.match(m.err,/^stack-release: refusing marker-failed: could not make the rollback set .*; nothing was changed\n$/);
|
|
const b=lastAct(e);assert.notEqual(b,a);
|
|
assert.deepEqual(mine(e,b).map((o)=>[o.result,o.code]),[['started',undefined],['refused','marker-failed']]);
|
|
chmodSync(join(e.state,'stack-rollback'),0o755);
|
|
assert.deepEqual(open(e),[]);
|
|
});
|
|
|
|
test('row 232: confirm and revert refuse open-activation with no marker, two, the wrong copy, or one the log holds closed; refusals name no activation',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const tryBoth=async(id,why)=>{
|
|
for(const v of ['confirm','revert']){
|
|
const o=open(e),logs=logLines(e).length;
|
|
const r=await run(x,v,id,'acme');assert.equal(r.code,3,`${v} ${why}: ${r.out}${r.err}`);
|
|
assert.match(r.err,/^stack-release: refusing open-activation: /,why);
|
|
assert.deepEqual(logLines(e).slice(logs).map((l)=>[l.result,l.code,l.activation,l.op]),[['refused','open-activation',null,v]],why);
|
|
assert.deepEqual(open(e),o,why);
|
|
}
|
|
};
|
|
await tryBoth(C1,'no marker');
|
|
// One marker whose activation the log never started. It sorts before any real one, so a confirm
|
|
// that took the newest of two markers would take the real one.
|
|
const fake=join(e.state,'stack-rollback','20000101T000000Z-'+C1.slice(0,12));
|
|
mkdirSync(fake,{recursive:true});writeFileSync(join(fake,'AWAITING-UNITS'),`${C1}\n`);
|
|
await tryBoth(C1,'marker not open');
|
|
rmSync(fake,{recursive:true});
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
await tryBoth(B,'wrong copy');
|
|
mkdirSync(fake);writeFileSync(join(fake,'AWAITING-UNITS'),`${C1}\n`);
|
|
await tryBoth(C1,'two markers');
|
|
rmSync(fake,{recursive:true});
|
|
// gate refuses nothing and writes nothing, open or not.
|
|
const logs=readFileSync(e.log,'utf8');
|
|
const g=await run(x,'gate',C1,'acme','--since','1','--timeout','1');assert.equal(g.code,1);
|
|
assert.equal(readFileSync(e.log,'utf8'),logs);assert.deepEqual(open(e),[a]);
|
|
});
|
|
|
|
test('row 232: confirm polls: bus status good after 3 s fails with --timeout 1, writes FAILED and leaves the rest; FAILED then refuses activation-failed',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
up(e,C1);
|
|
const p0=readFileSync(e.pointer,'utf8'),l0=readFileSync(e.log,'utf8');
|
|
fault(e,`status-until-${C1}`,String(Date.now()+3000));
|
|
const c=await run(x,'confirm',C1,'acme','--timeout','1');assert.equal(c.code,1,c.out+c.err);
|
|
assert.match(c.err,new RegExp(`^stack-release: activate failed with bus-status; ${a} stays open`));
|
|
assert.equal(readFileSync(join(rs(e,a),'FAILED'),'utf8'),'bus-status\n');
|
|
assert.equal(readFileSync(e.pointer,'utf8'),p0);assert.equal(readFileSync(e.log,'utf8'),l0);
|
|
assert.ok(existsSync(join(rs(e,a),'AWAITING-UNITS')));
|
|
const f=await run(x,'confirm',C1,'acme');assert.equal(f.code,3);
|
|
assert.match(f.err,new RegExp(`^stack-release: refusing activation-failed: ${a} has a FAILED file`));
|
|
assert.equal(logLines(e).at(-1).activation,null);assert.deepEqual(open(e),[a]);
|
|
});
|
|
|
|
test('row 232: confirm polls to a pass with --timeout 10: the pointer, an ok line that reads as the started run, no marker',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
up(e,C1);
|
|
fault(e,`status-until-${C1}`,String(Date.now()+3000));
|
|
const c=await run(x,'confirm',C1,'acme','--timeout','10');assert.equal(c.code,0,c.out+c.err);
|
|
assert.equal(c.out.split('\n').at(-2),`stack-release: activate ok: ${C1} is live for acme`);
|
|
const p=pointer(e);assert.deepEqual([p.copy,p.activation,p.schema,p.pm,p.console],[C1,a,'3g',false,['--port',e.port,'--business','acme']]);
|
|
const f=sameAsStarted(e,a);assert.deepEqual([f.result,f.op,f.from,f.to,f.backup],['ok','activate',B,C1,a]);
|
|
assert.ok(!existsSync(join(rs(e,a),'AWAITING-UNITS')));
|
|
assert.deepEqual(journalCalls(e),[]);
|
|
});
|
|
|
|
// N4 with the brief's numbers: the started line at 1792540800.900 floors to 1792540800. The host's
|
|
// clock runs so boot status is after that second.
|
|
const T0=1792540800,offsetTo=(s)=>Math.floor(Date.now()/1000)-s;
|
|
function startedAt(e,a,ms){
|
|
const l=readFileSync(e.log,'utf8').split('\n').map((x)=>{if(!x)return x;const o=JSON.parse(x);if(o.activation===a&&o.result==='started')o.at=new Date(ms).toISOString();return JSON.stringify(o)});
|
|
writeFileSync(e.log,l.join('\n'));
|
|
}
|
|
|
|
test('row 232: stale-start: @1792540799 fails a start floor of 1792540800 with stale-start',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
startedAt(e,a,T0*1000+900);up(e,C1,{offset:offsetTo(T0+100)});
|
|
fault(e,'enter',JSON.stringify({value:T0-1}));
|
|
const c=await run(x,'confirm',C1,'acme','--timeout','1');assert.equal(c.code,1,c.out+c.err);
|
|
assert.ok(c.out.includes(`gate failed on ${C1}: stale-start (the bus started at @${T0-1}, before @${T0})`),c.out);
|
|
assert.equal(readFileSync(join(rs(e,a),'FAILED'),'utf8'),'stale-start\n');
|
|
});
|
|
|
|
test('row 232: stale-start is a gate code: an enter time that turns to @1792540800 after 2 s passes with --timeout 10',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
startedAt(e,a,T0*1000+900);up(e,C1,{offset:offsetTo(T0+100)});
|
|
fault(e,'enter',JSON.stringify({value:T0-1,at:Date.now()+2000,then:T0}));
|
|
const c=await run(x,'confirm',C1,'acme','--timeout','10');assert.equal(c.code,0,c.out+c.err);
|
|
assert.equal(pointer(e).copy,C1);
|
|
});
|
|
|
|
test('row 232: gate (N2, A2f): --since 1792540800 passes @1792540800 and fails @1792540799; it writes nothing',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
up(e,B,{offset:offsetTo(T0+100)});
|
|
const snap=()=>[readFileSync(e.log,'utf8'),readFileSync(e.pointer,'utf8'),tree(e.state).join('\n')];
|
|
const s0=snap();
|
|
fault(e,'enter',JSON.stringify({value:T0}));
|
|
const g=await run(x,'gate',B,'acme','--since',String(T0));assert.equal(g.code,0,g.out+g.err);
|
|
assert.equal(g.out,`stack-release: gate passed on ${B}\n`);
|
|
fault(e,'enter',JSON.stringify({value:T0-1}));
|
|
const f=await run(x,'gate',B,'acme','--since',String(T0),'--timeout','1');assert.equal(f.code,1,f.out+f.err);
|
|
assert.ok(f.out.includes(`gate failed on ${B}: stale-start (the bus started at @${T0-1}, before @${T0})`),f.out);
|
|
assert.deepEqual(snap(),s0);
|
|
const u=await run(x,'gate',B,'acme');assert.equal(u.code,4);
|
|
assert.deepEqual(journalCalls(e),[]);
|
|
});
|
|
|
|
test('row 232: the external PM check reads launches list: one live run of launch.by since the start passes',async(t)=>{
|
|
const {e,x}=await xseed(t,{pm:true});
|
|
const cases=[['',0,''],['coder',0,''],['none',1,'no live launch of chief'],['twolive',1,'2 live launches of chief'],['early',1,'started before this start'],['dead',1,'no live launch of chief'],['other',1,'the launch of chief is for other']];
|
|
for(const [mode,code,why] of cases){
|
|
rmSync(join(e.sd,'launches.json'),{force:true});
|
|
if(mode)fault(e,`pm-${B}`,mode);else rmSync(join(e.sd,'faults',`pm-${B}`),{force:true});
|
|
const since=Math.floor(Date.now()/1000);
|
|
up(e,B,{pm:true});
|
|
const g=await run(x,'gate',B,'acme','--since',String(since),'--timeout','1');assert.equal(g.code,code,`${mode}: ${g.out}${g.err}`);
|
|
if(why)assert.ok(g.out.includes(`gate failed on ${B}: pm-launch-unconfirmed (`)&&g.out.includes(why),`${mode}: ${g.out}`);
|
|
down(e);
|
|
}
|
|
assert.deepEqual(journalCalls(e),[]);
|
|
});
|
|
|
|
test('row 232: revert puts the wrapper back and leaves the store when nothing migrated',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const w0=sha(join(e.bin,'mosaic'));
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
// A write to the stopped store after A3: revert has no MIGRATING, so it is not its to undo.
|
|
const db=new DatabaseSync(e.store);db.prepare('INSERT INTO events (at) VALUES (?)').run('2001-01-01T00:00:00.000Z');db.close();
|
|
const s1=storeSha(e);assert.notEqual(s1,sha(backupStore(e,a)));
|
|
const v=await run(x,'revert',C1,'acme');assert.equal(v.code,0,v.out+v.err);
|
|
assert.equal(v.out.split('\n').at(-2),`stack-release: revert ok: ${a}'s wrapper is back; root installs the old units next`);
|
|
assert.equal(sha(join(e.bin,'mosaic')),w0);assert.equal(storeSha(e),s1);
|
|
assert.deepEqual(mine(e,a).map((o)=>o.result),['started']);assert.ok(existsSync(join(rs(e,a),'AWAITING-UNITS')));
|
|
});
|
|
|
|
test('row 232: confirm --rollback refuses rollback-mismatch, and logs rollback-failed with exit 2 when OLD fails its gate',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
const v=await run(x,'revert',C1,'acme');assert.equal(v.code,0,v.out+v.err);
|
|
const m=await run(x,'confirm',C1,'acme','--rollback',C,'--cause','A5');assert.equal(m.code,3);
|
|
assert.equal(m.err,`stack-release: refusing rollback-mismatch: ${C} is not the copy ${a} started from, ${B}\n`);
|
|
assert.equal(logLines(e).at(-1).activation,null);assert.deepEqual(open(e),[a]);
|
|
fault(e,`healthz-${B}`);up(e,B);
|
|
const f=await run(x,'confirm',C1,'acme','--rollback',B,'--cause','A5','--timeout','1');assert.equal(f.code,2,f.out+f.err);
|
|
assert.equal(f.err,`stack-release: rollback-failed: the run failed with unrecorded and the rollback with healthz; root stops the units. Rollback set: ${rs(e,a)}, backup ${join(e.data,'backups','stack',a)}\n`);
|
|
const l=sameAsStarted(e,a);assert.deepEqual([l.result,l.code,l.rollback,l.rollbackCode],['rollback-failed','unrecorded','failed','healthz']);
|
|
for(const n of ['AWAITING-UNITS','FAILED'])assert.ok(!existsSync(join(rs(e,a),n)),n);
|
|
assert.deepEqual(open(e),[]);
|
|
});
|
|
|
|
test('row 232: confirm --rollback after a --restore swap logs rollbackCode not-attempted; rollback --restore finds an external backup',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C,'--migrate');assert.equal(r.code,0,r.out+r.err);
|
|
up(e,C);assert.equal((await run(x,'confirm',C,'acme')).code,0);
|
|
down(e);
|
|
const b=await run(x,'rollback','acme','--to',B,'--restore',a);assert.equal(b.code,0,b.out+b.err);
|
|
const ra=lastAct(e);
|
|
assert.ok(b.out.includes(`restore: ${join(e.data,'backups','stack',a)}; `),b.out);
|
|
assert.ok(existsSync(join(rs(e,ra),'RESTORED')));assert.equal(readFileSync(join(rs(e,ra),'BACKUP'),'utf8'),`${a}\n`);
|
|
assert.equal(storeSha(e),sha(backupStore(e,a)));
|
|
const f=await run(x,'confirm',B,'acme','--rollback',C,'--cause','A4');assert.equal(f.code,2,f.out+f.err);
|
|
const l=sameAsStarted(e,ra);assert.deepEqual([l.op,l.result,l.code,l.rollbackCode,l.backup],['rollback','rollback-failed','units-start','not-attempted',a]);
|
|
});
|
|
|
|
// --- B1: real sequences on one log ---
|
|
|
|
test('row 232: sequence 1: activate, confirm times out, revert, confirm --rollback --cause A5: gate-failed rollback ok',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const w0=sha(join(e.bin,'mosaic')),p0=readFileSync(e.pointer,'utf8');
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
fault(e,`healthz-${C1}`);up(e,C1);
|
|
const c=await run(x,'confirm',C1,'acme','--timeout','1');assert.equal(c.code,1,c.out+c.err);
|
|
assert.deepEqual(mine(e,a).map((o)=>o.result),['started']);
|
|
down(e);
|
|
assert.equal((await run(x,'revert',C1,'acme')).code,0);assert.equal(sha(join(e.bin,'mosaic')),w0);
|
|
up(e,B);
|
|
const k=await run(x,'confirm',C1,'acme','--rollback',B,'--cause','A5');assert.equal(k.code,1,k.out+k.err);
|
|
assert.equal(k.err,`stack-release: healthz; rolled back to ${B}, which passed the gate\n`);
|
|
const l=sameAsStarted(e,a);assert.deepEqual([l.result,l.code,l.rollback],['gate-failed','healthz','ok']);
|
|
assert.equal(readFileSync(e.pointer,'utf8'),p0);
|
|
await closed(e,x,a);
|
|
});
|
|
|
|
test('row 232: sequence 2: activate --migrate, the wrapper install fails, revert restores TAKEN, confirm --rollback --cause A3',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const s0=storeSha(e);
|
|
chmodSync(e.bin,0o555);
|
|
const {r,a}=await stage(e,x,C,'--migrate');chmodSync(e.bin,0o755);
|
|
assert.equal(r.code,1,r.out+r.err);assert.match(r.err,new RegExp(`^stack-release: activate failed with wrapper; ${a} stays open`));
|
|
assert.deepEqual(mine(e,a).map((o)=>o.result),['started']);
|
|
for(const n of ['AWAITING-UNITS','MIGRATING'])assert.ok(existsSync(join(rs(e,a),n)),n);
|
|
assert.equal(readFileSync(join(rs(e,a),'FAILED'),'utf8'),'wrapper\n');
|
|
assert.equal(storeVersion(e.store,false),'3h');assert.equal(sha(backupStore(e,a)),s0);
|
|
const v=await run(x,'revert',C,'acme');assert.equal(v.code,0,v.out+v.err);
|
|
assert.equal(v.out.split('\n').at(-2),`stack-release: revert ok: ${a}'s wrapper and store are back; root installs the old units next`);
|
|
assert.equal(storeSha(e),s0);
|
|
// revert runs again: the store already matches, and nothing moves.
|
|
const posts=readdirSync(e.data).filter((n)=>n.startsWith('bus.post-attempt-'));
|
|
assert.equal((await run(x,'revert',C,'acme')).code,0);
|
|
assert.deepEqual(readdirSync(e.data).filter((n)=>n.startsWith('bus.post-attempt-')),posts);
|
|
up(e,B);
|
|
const k=await run(x,'confirm',C,'acme','--rollback',B,'--cause','A3');assert.equal(k.code,1,k.out+k.err);
|
|
const l=sameAsStarted(e,a);assert.deepEqual([l.result,l.code,l.rollback],['failed','wrapper','ok']);
|
|
await closed(e,x,a);
|
|
});
|
|
|
|
test('row 232: sequence 3: activate, root\'s A4 fails, revert, confirm --rollback --cause A4: failed code units-start',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
assert.equal((await run(x,'revert',C1,'acme')).code,0);
|
|
up(e,B);
|
|
const k=await run(x,'confirm',C1,'acme','--rollback',B,'--cause','A4');assert.equal(k.code,1,k.out+k.err);
|
|
assert.equal(k.err,`stack-release: units-start; rolled back to ${B}, which passed the gate\n`);
|
|
const l=sameAsStarted(e,a);assert.deepEqual([l.result,l.code,l.rollback],['failed','units-start','ok']);
|
|
await closed(e,x,a);
|
|
});
|
|
|
|
test('row 232: sequence 4: a capture failure leaves nothing open, then A2f\'s gate OLD writes nothing',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
fault(e,'no-fragment');
|
|
const r=await run(x,'activate',C1,'acme');assert.equal(r.code,3,r.out+r.err);rmSync(join(e.sd,'faults','no-fragment'));
|
|
const a=lastAct(e);assert.deepEqual(mine(e,a).map((o)=>[o.result,o.code]),[['started',undefined],['refused','capture-failed']]);
|
|
assert.deepEqual(open(e),[]);
|
|
const since=Math.floor(Date.now()/1000);up(e,B);
|
|
const s0=[readFileSync(e.log,'utf8'),readFileSync(e.pointer,'utf8'),tree(e.state).join('\n')];
|
|
const g=await run(x,'gate',B,'acme','--since',String(since));assert.equal(g.code,0,g.out+g.err);
|
|
assert.deepEqual([readFileSync(e.log,'utf8'),readFileSync(e.pointer,'utf8'),tree(e.state).join('\n')],s0);
|
|
});
|
|
|
|
test('row 232: sequence 5: a confirm of the wrong copy is refused, and revert and confirm --rollback still close the activation',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
up(e,C1);
|
|
const w=await run(x,'confirm',B,'acme');assert.equal(w.code,3);assert.match(w.err,/^stack-release: refusing open-activation: /);
|
|
assert.equal(logLines(e).at(-1).activation,null);assert.deepEqual(open(e),[a]);
|
|
down(e);
|
|
assert.equal((await run(x,'revert',C1,'acme')).code,0);
|
|
up(e,B);
|
|
const k=await run(x,'confirm',C1,'acme','--rollback',B,'--cause','A5');assert.equal(k.code,1,k.out+k.err);
|
|
const l=sameAsStarted(e,a);assert.deepEqual([l.result,l.code,l.rollback],['gate-failed','unrecorded','ok']);
|
|
await closed(e,x,a);
|
|
});
|
|
|
|
test('row 232: sequence 6: a revert that fails leaves the activation open, and the next --plan refuses interrupted-run',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const {r,a}=await stage(e,x,C1);assert.equal(r.code,0,r.out+r.err);
|
|
rmSync(join(rs(e,a),'wrapper'));
|
|
const v=await run(x,'revert',C1,'acme');assert.equal(v.code,1,v.out+v.err);
|
|
assert.equal(v.err,`stack-release: revert failed: the rollback set has no saved wrapper; ${a} stays open. Rollback set: ${rs(e,a)}\n`);
|
|
assert.deepEqual(open(e),[a]);assert.ok(existsSync(join(rs(e,a),'AWAITING-UNITS')));
|
|
const p=await run(x,'activate',C,'acme','--plan','--migrate');assert.equal(p.code,3,p.out+p.err);
|
|
assert.ok(p.out.includes(`refuses interrupted-run: activation ${a} started and has no final line`),p.out);
|
|
});
|
|
|
|
test('row 232: sequence 7: adopt, two confirmed activates, rollback --to the previous copy, confirmed, logs op rollback',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const one=await stage(e,x,C1);assert.equal(one.r.code,0,one.r.out+one.r.err);
|
|
up(e,C1);assert.equal((await run(x,'confirm',C1,'acme')).code,0);down(e);
|
|
const two=await stage(e,x,C,'--migrate');assert.equal(two.r.code,0,two.r.out+two.r.err);
|
|
up(e,C);assert.equal((await run(x,'confirm',C,'acme')).code,0);down(e);
|
|
const l1=sameAsStarted(e,one.a),l2=sameAsStarted(e,two.a);
|
|
assert.deepEqual([l1.op,l1.result,l1.from,l1.to,l1.backup],['activate','ok',B,C1,one.a]);
|
|
assert.deepEqual([l2.op,l2.result,l2.from,l2.to,l2.backup],['activate','ok',C1,C,two.a]);
|
|
// N5: --expect-from names the copy root read from its units.
|
|
const d1=await run(x,'activate',C1,'acme','--plan','--expect-from',C1);assert.equal(d1.code,3,d1.out+d1.err);
|
|
assert.ok(d1.out.includes(`refuses pointer-differs: the pointer names ${C} and root's units name ${C1}; Jason or Mos settles it`),d1.out);
|
|
const d2=await run(x,'rollback','acme','--to',C1,'--plan','--expect-from',B);assert.equal(d2.code,3,d2.out+d2.err);
|
|
assert.ok(d2.out.includes(`refuses pointer-differs: the pointer names ${C} and root's units name ${B}`),d2.out);
|
|
const ok=await run(x,'rollback','acme','--to',C1,'--restore',two.a,'--plan','--expect-from',C);assert.equal(ok.code,0,ok.out+ok.err);
|
|
const logs=logLines(e).length;
|
|
const no=await run(x,'rollback','acme','--to',B,'--plan');assert.equal(no.code,3);
|
|
assert.equal(no.err,`stack-release: refusing to-not-previous: --to ${B} is not the previous copy; the log names ${C1}\n`);
|
|
assert.equal(logLines(e).length,logs);
|
|
const nr=await run(x,'rollback','acme','--to',B,'--restore',one.a);assert.equal(nr.code,3);assert.match(nr.err,/^stack-release: refusing to-not-previous: /);
|
|
assert.deepEqual(logLines(e).slice(logs).map((o)=>[o.result,o.code,o.activation]),[['refused','to-not-previous',null]]);
|
|
const rb=await run(x,'rollback','acme','--to',C1,'--restore',two.a);assert.equal(rb.code,0,rb.out+rb.err);
|
|
const ra=lastAct(e);
|
|
up(e,C1);
|
|
const k=await run(x,'confirm',C1,'acme');assert.equal(k.code,0,k.out+k.err);
|
|
assert.equal(k.out.split('\n').at(-2),`stack-release: rollback ok: ${C1} is live for acme`);
|
|
const l3=sameAsStarted(e,ra);assert.deepEqual([l3.op,l3.result,l3.from,l3.to,l3.backup],['rollback','ok',C,C1,two.a]);
|
|
assert.deepEqual([pointer(e).copy,pointer(e).schema],[C1,'3g']);
|
|
const prev=await run(x,'rollback','acme','--to',B,'--plan');assert.match(prev.err,new RegExp(`the log names ${C}\\n$`));
|
|
await closed(e,x,ra);
|
|
});
|
|
|
|
test('row 232: sequence 8: a migration that writes and fails: revert restores the backup byte for byte, confirm --rollback --cause A3',async(t)=>{
|
|
const {e,x}=await xseed(t);
|
|
const s0=storeSha(e);
|
|
fault(e,'migrate-fails');
|
|
const {r,a}=await stage(e,x,C,'--migrate');assert.equal(r.code,1,r.out+r.err);rmSync(join(e.sd,'faults','migrate-fails'));
|
|
assert.equal(readFileSync(join(rs(e,a),'FAILED'),'utf8'),'migrate-failed\n');assert.ok(existsSync(join(rs(e,a),'MIGRATING')));
|
|
assert.equal(storeVersion(e.store,false),'3h-partial');
|
|
assert.equal((await run(x,'revert',C,'acme')).code,0);
|
|
assert.equal(storeSha(e),s0);assert.equal(storeSha(e),sha(backupStore(e,a)));
|
|
assert.deepEqual(readdirSync(join(e.data,'bus')),readdirSync(join(e.data,'backups','stack',a,'bus')));
|
|
up(e,B);
|
|
const k=await run(x,'confirm',C,'acme','--rollback',B,'--cause','A3');assert.equal(k.code,1,k.out+k.err);
|
|
const l=sameAsStarted(e,a);assert.deepEqual([l.result,l.code,l.rollback],['failed','migrate-failed','ok']);
|
|
await closed(e,x,a);
|
|
});
|
|
|
|
// --- User mode against the base ---
|
|
|
|
// The fixture repo with the base's two scripts: everything else is the same tree and history.
|
|
const baseRepo=join(base,'base-repo');
|
|
cpSync(repo,baseRepo,{recursive:true});
|
|
for(const f of ['scripts/stack-release.sh','scripts/stack-copy.sh'])writeFileSync(join(baseRepo,f),execFileSync('git',['-C',ROOT,'show',`${BASE}:${f}`]),{mode:0o755});
|
|
const BASE_SR=join(baseRepo,'scripts','stack-release.sh');
|
|
|
|
test('row 232: user mode: every systemctl call carries --user, and activate, rollback and check make the base\'s calls',async(t)=>{
|
|
const calls=async(sr,dir)=>{
|
|
const e=await mk(t);e.sr=sr;await seed(e);
|
|
const n=lines(join(e.sd,'argv.log')).length;
|
|
const a=await run(e,'activate',C1,'acme');assert.equal(a.code,0,a.out+a.err);
|
|
const r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
|
execFileSync('bash',[join(dir,'scripts','stack-copy.sh'),'check','--copies',COPIES,'--bin',e.bin,'--business','acme'],{env:e.env,stdio:'ignore'});
|
|
return lines(join(e.sd,'argv.log')).slice(n).map((l)=>l.split(e.root).join('<root>').split(e.port).join('<port>'));
|
|
};
|
|
const cand=await calls(undefined,repo),old=await calls(BASE_SR,baseRepo);
|
|
assert.ok(cand.length>20);
|
|
for(const c of cand)assert.match(c,/^--user /,c);
|
|
assert.deepEqual(cand,old);
|
|
});
|
|
|
|
test('row 232: user mode: a default activate --plan prints the base\'s bytes',async(t)=>{
|
|
const e=await mk(t);await seed(e);
|
|
for(const a of [['activate',C1,'acme','--plan'],['activate',C,'acme','--plan','--migrate'],['activate',C,'acme','--plan'],['rollback','acme','--plan']]){
|
|
const cand=await run(e,...a),old=await run({...e,sr:BASE_SR},...a);
|
|
assert.deepEqual(cand,old,a.join(' '));
|
|
}
|
|
assert.ok(!existsSync(join(e.state,'stack-rollback')));
|
|
});
|