row 187 (#1685): activate sets, carries and drops the Console's proof pair
Row 178 gave the Console --proof-header NAME --proof-file PATH, but on the pinned stack only activate --tester-port could write a new Console flag into the pointer. A hand install of the pair would make every later activate, pm-on and rollback refuse unit-flags-drift. activate now takes the pair, which replaces any pair in the pointer, and --no-proof, which drops it. The pair goes after the preview flags and before --send and --tester-port, the order webui-service.sh render writes and flags_drift compares. With neither flag, a target Console that takes --proof-header carries the pair; a rollback to one that doesn't drops it and says so, and an activate to one refuses proof-target unless --no-proof is given. An activate may name the pointer's copy only when it sets a pair or drops one with --no-proof, so Part B's B3 and its corrected rollback line B-R1 (activate <live copy> --pm --no-proof) can run; every other activate of the live copy still refuses already-active. Before anything stops, the run refuses proof-needs-preview, proof-header-invalid (proofOptions' own rules) and proof-file-invalid: not absolute, a character the unit can't carry, a symlink, missing, not a regular file, inside the repository or the copies, mode other than 600 or 400, or another owner. The file gets lstat, stat and realpath only; the script never opens it and no line it writes holds the value. --plan prints one proof: line. docs/TOOLS.md "Pinned stack update" lists the flags and describes the pair. stack-release.test.mjs adds T53, eleven subtests. Each ends with a scan of every written file and output for the proof file's sentinel and an inotify watch that counts no open or read of it. The five brief mutants and Darkwing's two are killed. Filbert's M8 (a carried pair skips the checks) and M9 (the repository case dropped) survive T53; both are follow-ups on P19, with a stale comment in proof_for_target and the stale row 178 paragraph in docs/TOOLS.md. This is step A0 of mos-0928 Part B and builds the tool only; nothing turns the proof on for the live Console. Filbert approved round 1 on #1685.
This commit is contained in:
+24
-1
@@ -367,7 +367,8 @@ reports checkout or copy identity and refactor drift, and writes nothing.
|
||||
|
||||
```
|
||||
scripts/stack-release.sh adopt BUSINESS
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh status BUSINESS
|
||||
@@ -457,6 +458,28 @@ refuses `capture-failed` before it stops anything. Adopt uses the same rule.
|
||||
8. Jason's plain `mosaic talk pm "..."` must print sent and a reply. Keep the
|
||||
`status` output with the receipts.
|
||||
|
||||
The Console's row 178 proof pair gets into the pointer only through `activate`
|
||||
(row 187). `--proof-header NAME --proof-file PATH` puts the pair in the
|
||||
pointer's Console flags and the unit after the preview flags and before `--send`
|
||||
and `--tester-port`, where `webui-service.sh` renders it, replacing any pair
|
||||
already there. `--no-proof` drops it. Either may name the live copy, which a
|
||||
plain activate refuses with `already-active`, so the proof goes on or off
|
||||
without changing copies; `--no-proof` with no pair in the pointer still refuses
|
||||
there. Later activates, `pm-on` and rollbacks carry the pair unchanged. A
|
||||
rollback to a copy whose Console lacks `--proof-header` drops the pair and says
|
||||
so in the steps log. An activate to such a copy refuses `proof-target` unless
|
||||
`--no-proof` is given, so the proof goes off only when asked. Before anything
|
||||
stops, the run also refuses `proof-needs-preview` when the flags it would
|
||||
install lack `--public-host`, `--auth-header` or `--allow-user`,
|
||||
`proof-header-invalid` on the Console's own header rules, and
|
||||
`proof-file-invalid` for a path that isn't absolute, holds a character outside
|
||||
`[A-Za-z0-9/._+-]`, is a symlink or not a regular file, isn't mode 600 or 400,
|
||||
isn't owned by the uid the script runs as, or sits inside the repository or the
|
||||
copies. It checks the file with `stat` and `realpath` and never reads the value.
|
||||
`--plan` prints one `proof:` line: `on`, `carried`, `dropped` with the reason,
|
||||
or `off`. On the live copy it also notes that the run re-pins the pointer's copy
|
||||
and only the Console flags change.
|
||||
|
||||
#### A target past c216f47db: the PM after the harness kind
|
||||
|
||||
From c216f47db on, the bus launches the PM only with a `harness` kind in the
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
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 {mkdtempSync,mkdirSync,writeFileSync,readFileSync,appendFileSync,cpSync,rmSync,chmodSync,existsSync,statSync,lstatSync,symlinkSync,readdirSync,realpathSync} from 'node:fs';
|
||||
import {createServer} from 'node:http';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {DatabaseSync} from 'node:sqlite';
|
||||
@@ -262,7 +262,7 @@ const KINDS_OLD=KINDS_NEW.replace('\n harness: Object.freeze({','\n "harness-b
|
||||
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]]){
|
||||
for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?"// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n":'// 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});
|
||||
@@ -381,7 +381,7 @@ async function seed(e,{copy=B,pm=false,store='3g',pointer:how='adopt'}={}){
|
||||
const rolledBack=(code,to=B)=>`stack-release: ${code}; rolled back to ${to}, which passed the gate\n`;
|
||||
const planBackup=(e)=>`backup: the run backs up the stopped store to ${e.data}/backups/stack/<activation>/ before any migration`;
|
||||
const planNoBackup='backup: none; the run rolls back before the backup';
|
||||
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}`];
|
||||
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];
|
||||
|
||||
// --- The T table ---
|
||||
|
||||
@@ -880,7 +880,7 @@ async function pending(e){
|
||||
// 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);
|
||||
const r=await run(e,...args);assert.equal(r.code,3,r.out+r.err);(e.outs??=[]).push(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]);
|
||||
@@ -1030,17 +1030,17 @@ test('T36 --plan shows the pending state and lists the row 169 refusals it can s
|
||||
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),'']);
|
||||
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,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`,'']);
|
||||
assert.deepEqual(out.slice(-7),['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}`,'']);
|
||||
assert.deepEqual(out.slice(-7),['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),'']);
|
||||
assert.deepEqual(out.slice(-6),['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.
|
||||
@@ -1421,3 +1421,232 @@ test('T178 proxy proof pair survives a tester-port replacement as well',async t=
|
||||
assert.deepEqual(pointer(e).console,[...expected,'--tester-port','7333']);
|
||||
assert.ok(consoleText(e).includes([...expected,'--tester-port','7333'].join(' ')));
|
||||
});
|
||||
|
||||
// Row 187 (#1685): activate sets, carries and drops the proof pair. Every proof file here holds
|
||||
// SENTINEL, which no output, unit, pointer, log or steps log may hold (brief test 8), and an
|
||||
// inotify watch counts each open or read of it, which must stay 0: the script checks the file
|
||||
// with lstat and stat only. Python's ctypes reaches inotify(7); node's fs.watch has no IN_ACCESS.
|
||||
const SENTINEL='row187_sentinel_proof_value_0123456789abcdef';
|
||||
const PREVIEW=['--public-host','console.example','--auth-header','x-user','--allow-user','jason'];
|
||||
const WATCH=`import ctypes,os,select,struct,sys
|
||||
libc=ctypes.CDLL(None,use_errno=True)
|
||||
fd=libc.inotify_init1(0)
|
||||
if fd<0 or libc.inotify_add_watch(fd,os.fsencode(sys.argv[1]),0x21)<0:sys.exit(2)
|
||||
print('ready',flush=True)
|
||||
n=0
|
||||
def drain(t):
|
||||
global n
|
||||
while select.select([fd],[],[],t)[0]:
|
||||
b=os.read(fd,65536);i=0
|
||||
while i<len(b):n+=1;i+=16+struct.unpack_from('iIII',b,i)[3]
|
||||
t=0
|
||||
while True:
|
||||
r=select.select([fd,0],[],[])[0]
|
||||
if fd in r:drain(0)
|
||||
if 0 in r and not os.read(0,1):break
|
||||
drain(0)
|
||||
print(n,flush=True)`;
|
||||
// A subtest that fails before noLeak leaves its watches running; they end with the file.
|
||||
const WATCHERS=new Set();
|
||||
after(()=>{for(const ch of WATCHERS)ch.kill('SIGKILL')});
|
||||
// A proof file under e.root, outside the data root, with its read watch running.
|
||||
async function proofFile(e,name='proof.187',mode=0o600){
|
||||
const f=join(e.root,name);writeFileSync(f,SENTINEL+'\n',{mode});chmodSync(f,mode);
|
||||
const ch=spawn('python3',['-I','-c',WATCH,f],{stdio:['pipe','pipe','inherit']});
|
||||
let out='';ch.stdout.on('data',(d)=>{out+=d});
|
||||
await new Promise((res,rej)=>{ch.stdout.on('data',()=>{if(out.startsWith('ready\n'))res()});ch.on('error',rej);ch.on('close',(c)=>rej(new Error(`inotify watch exited ${c}`)))});
|
||||
WATCHERS.add(ch);ch.on('close',()=>WATCHERS.delete(ch));
|
||||
(e.watches??=[]).push(async()=>{ch.stdin.end();if(ch.exitCode===null)await once(ch,'close');return Number(out.split('\n')[1])});
|
||||
return f;
|
||||
}
|
||||
async function prun(e,...args){const r=await run(e,...args);(e.outs??=[]).push(r.out,r.err);return r}
|
||||
// Brief test 8 and the read mutant, at the end of each case.
|
||||
async function noLeak(e){
|
||||
const hits=[];
|
||||
const walk=(q)=>{let st;try{st=lstatSync(q)}catch{return}if(st.isDirectory()){for(const n of readdirSync(q))walk(join(q,n))}else if(st.isFile()&&readFileSync(q).includes(SENTINEL))hits.push(q)};
|
||||
for(const d of [e.data,e.units,e.sd,e.bin])walk(d);
|
||||
(e.outs??[]).forEach((o,i)=>{if(o.includes(SENTINEL))hits.push(`run output ${i}`)});
|
||||
assert.deepEqual(hits,[]);
|
||||
for(const stop of e.watches??[])assert.equal(await stop(),0,'the run opened or read the proof file');
|
||||
}
|
||||
// The seeded stack with the preview flags, and any flags in more, in the pointer and the Console unit.
|
||||
async function seedPreview(e,opts,more=[]){
|
||||
await seed(e,opts);
|
||||
const p=pointer(e);p.console.push(...PREVIEW,...more);writeFileSync(e.pointer,JSON.stringify(p)+'\n');
|
||||
edit(join(e.units,CON),' --business acme\n',` --business acme ${[...PREVIEW,...more].join(' ')}\n`);
|
||||
return p.console;
|
||||
}
|
||||
const pair=(f,h='x-console-proof')=>['--proof-header',h,'--proof-file',f];
|
||||
const conExec=(e)=>consoleText(e).split('\n').find((l)=>l.startsWith('ExecStart='));
|
||||
// --plan: exit code, nothing written, nothing started or stopped, no log line; the output lines.
|
||||
async function planOnly(e,code,...args){
|
||||
const before=fp(e),data=tree(e.data),n=calls(e).length,logs=logLines(e).length;
|
||||
const r=await prun(e,...args,'--plan');assert.equal(r.code,code,r.out+r.err);assert.equal(r.err,'');
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(tree(e.data),data);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
return r.out.split('\n');
|
||||
}
|
||||
const proofLines=(out)=>out.filter((l)=>l.startsWith('proof:'));
|
||||
const CARRY_MSG=(to)=>`${to}'s Console does not support --proof-header, which the pointer's Console flags carry; give --no-proof to drop the pair`;
|
||||
const HEADER_MSG=(h)=>`--proof-header ${h} must be a header name of a-z, 0-9 and - (64 at most, lowercased) other than host, origin, x-mosaic-tester and the --auth-header value`;
|
||||
|
||||
test('T53 row 187: activate sets, carries and drops the proof pair',async(t)=>{
|
||||
await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),f=await proofFile(e),g=await proofFile(e,'proof2.187',0o400);
|
||||
const pre=base.slice(0,-1);
|
||||
let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);
|
||||
const out=await planOnly(e,0,'activate',C1,'acme',...pair(f));
|
||||
assert.deepEqual(proofLines(out),[`proof: on, header x-console-proof, file ${f}`]);
|
||||
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));
|
||||
r=await prun(e,'activate',C1,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);
|
||||
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
r=await prun(e,'activate',C1,'acme',...pair(g,'X-Proof-Two'));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
assert.equal(conExec(e).match(/--proof-header/g).length,1);assert.ok(!conExec(e).includes(f));
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('tests 2 and 3: an activate of the live copy writes the pair; a later activate and a rollback carry it',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e),f=await proofFile(e);
|
||||
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,[...base,...pair(f)]);assert.ok(conExec(e).endsWith(pair(f).join(' ')));
|
||||
assert.deepEqual(proofLines(await planOnly(e,0,'activate',C1,'acme')),['proof: carried, header x-console-proof']);
|
||||
r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,C1);assert.deepEqual(pointer(e).console,[...base,...pair(f)]);assert.ok(conExec(e).endsWith(pair(f).join(' ')));
|
||||
assert.deepEqual(proofLines(await planOnly(e,0,'rollback','acme')),['proof: carried, header x-console-proof']);
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,[...base,...pair(f)]);assert.ok(conExec(e).endsWith(pair(f).join(' ')));
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 2: only a pair change may name the live copy; the log keeps from and to, and rollback still goes to the earlier copy',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e),f=await proofFile(e);
|
||||
await refused(e,['activate',B,'acme'],'already-active',`${B} is the pointer's copy`);
|
||||
await refused(e,['activate',B,'acme','--no-proof'],'already-active',`${B} is the pointer's copy`);
|
||||
await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);
|
||||
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
r=await prun(e,'activate',C1,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
let l=logLines(e).at(-1);assert.deepEqual([l.op,l.from,l.to,l.result],['activate',C1,C1,'ok']);assert.ok(l.backup);
|
||||
assert.deepEqual(pointer(e).console,[...base,...pair(f)]);
|
||||
await refused(e,['activate',C1,'acme'],'already-active',`${C1} is the pointer's copy`);
|
||||
const out=await planOnly(e,0,'activate',C1,'acme','--no-proof');
|
||||
assert.deepEqual(proofLines(out),['proof: dropped; --no-proof drops header x-console-proof']);
|
||||
assert.ok(out.includes('commits: the run adds 0 (newest first, at most 200):'),out.join('\n'));
|
||||
r=await prun(e,'activate',C1,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
l=logLines(e).at(-1);assert.deepEqual([l.from,l.to,l.result],[C1,C1,'ok']);
|
||||
assert.equal(pointer(e).copy,C1);assert.deepEqual(pointer(e).console,base);assert.doesNotMatch(conExec(e),/--proof-/);
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,B);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 2: a live-copy activate says only the Console flags change, and backs up the stopped store before it installs',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e),f=await proofFile(e);
|
||||
const NOTE=`note: ${B} is the pointer's copy; the run re-pins it and only the Console flags change`;
|
||||
for(const args of [pair(f),['--no-proof']]){
|
||||
const out=await planOnly(e,0,'activate',B,'acme',...args);
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[NOTE]);assert.equal(out[out.indexOf(`to: ${B}`)+1],NOTE);
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('backup')),[planBackup(e)]);
|
||||
const sum=createHash('sha256').update(readFileSync(e.store)).digest('hex'),n=calls(e).length;
|
||||
const r=await prun(e,'activate',B,'acme',...args);assert.equal(r.code,0,r.out+r.err);
|
||||
const act=pointer(e).activation,l=logLines(e).at(-1);
|
||||
assert.deepEqual([l.from,l.to,l.result,l.backup],[B,B,'ok',act]);assert.equal(backupSum(e,act),sum);
|
||||
assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]);
|
||||
const steps=stepText(e),at=(x)=>steps.indexOf(x);
|
||||
assert.ok(at('stopped both units')>=0&&at('stopped both units')<at('backup: ')&&at('backup: ')<at(`installed `),steps);
|
||||
assert.deepEqual(pointer(e).console,args[0]==='--no-proof'?base:[...base,...pair(f)]);
|
||||
}
|
||||
assert.deepEqual(readdirSync(join(e.data,'backups','stack')).length,2);
|
||||
const out=await planOnly(e,0,'activate',C1,'acme');assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[]);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 3: pm-on keeps the pair',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,{pm:true}),f=await proofFile(e);
|
||||
let r=await prun(e,'activate',C1,'acme','--pm-after-harness',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
biz(e,HARNESS);
|
||||
r=await prun(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).pm,true);assert.deepEqual(pointer(e).console,[...base,...pair(f)]);assert.ok(conExec(e).endsWith(pair(f).join(' ')));
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 4: --no-proof drops the pair from the pointer and the unit and says so; with no pair it says so and passes',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e),f=await proofFile(e);
|
||||
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(proofLines(await planOnly(e,0,'activate',C1,'acme','--no-proof')),['proof: dropped; --no-proof drops header x-console-proof']);
|
||||
r=await prun(e,'activate',C1,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,base);assert.doesNotMatch(conExec(e),/--proof-/);
|
||||
assert.match(stepText(e),new RegExp(`drop --proof-header x-console-proof --proof-file ${f}; --no-proof was given\\n`));
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
assert.deepEqual(proofLines(await planOnly(e,0,'activate',B,'acme','--no-proof')),['proof: off; --no-proof was given and the pointer holds no pair']);
|
||||
r=await prun(e,'activate',B,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,base);assert.doesNotMatch(conExec(e),/--proof-/);
|
||||
assert.match(stepText(e),/--no-proof was given and the pointer holds no proof pair, so there is nothing to drop\n/);
|
||||
assert.deepEqual(proofLines(await planOnly(e,0,'activate',C1,'acme')),['proof: off']);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 5: each refusal exits 3 before anything stops',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e);
|
||||
const f=await proofFile(e),open=await proofFile(e,'open.187',0o644),loose=await proofFile(e,'loose.187',0o640);
|
||||
const link=join(e.root,'link.187');symlinkSync(f,link);
|
||||
const dir=join(e.root,'dir.187');mkdirSync(dir,{mode:0o700});
|
||||
const odd=join(e.root,'odd 187');writeFileSync(odd,SENTINEL+'\n',{mode:0o600});
|
||||
await refused(e,['activate',A,'acme',...pair(f)],'proof-target',`${A}'s Console does not support --proof-header`);
|
||||
for(const h of ['Host','origin','x-mosaic-tester','X-User','-lead','under_score','a'.repeat(65)]){
|
||||
await refused(e,['activate',C1,'acme',...pair(f,h)],'proof-header-invalid',HEADER_MSG(h));
|
||||
}
|
||||
const inCopies=join(COPIES,'proof-r187.187');writeFileSync(inCopies,SENTINEL+'\n',{mode:0o600});t.after(()=>rmSync(inCopies,{force:true}));
|
||||
await refused(e,['activate',C1,'acme',...pair(inCopies)],'proof-file-invalid',`--proof-file ${inCopies} is inside the repository or the copies`);
|
||||
rmSync(inCopies);
|
||||
for(const [p,why] of [['proof.187','is not an absolute path'],[odd,'holds a character outside [A-Za-z0-9/._+-]'],[link,'is a symlink'],[dir,'is not a regular file'],[join(e.root,'none.187'),'cannot be found'],[open,'has mode 644, not 600 or 400'],[loose,'has mode 640, not 600 or 400']]){
|
||||
await refused(e,['activate',C1,'acme',...pair(p)],'proof-file-invalid',`--proof-file ${p} ${why}`);
|
||||
}
|
||||
const g=await mk(t);await seed(g);const h=await proofFile(g);
|
||||
await refused(g,['activate',C1,'acme',...pair(h)],'proof-needs-preview','the proof pair needs --public-host, --auth-header and --allow-user in the Console flags; the run\'s flags lack --public-host --auth-header --allow-user');
|
||||
for(const x of [e,g])await noLeak(x);
|
||||
});
|
||||
await t.test('tests 5 and 9: --plan lists every proof refusal with the others and writes nothing',async(t)=>{
|
||||
const e=await mk(t);await seed(e);biz(e,HARNESS);const open=await proofFile(e,'open.187',0o644);
|
||||
const out=await planOnly(e,3,'activate',A,'acme',...pair(open,'host'));
|
||||
assert.deepEqual(proofLines(out),[`proof: on, header host, file ${open}`]);
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[
|
||||
`refuses proof-target: ${A}'s Console does not support --proof-header`,
|
||||
'refuses proof-needs-preview: the proof pair needs --public-host, --auth-header and --allow-user in the Console flags; the run\'s flags lack --public-host --auth-header --allow-user',
|
||||
`refuses proof-header-invalid: ${HEADER_MSG('host')}`,
|
||||
`refuses proof-file-invalid: --proof-file ${open} has mode 644, not 600 or 400`,
|
||||
`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`]);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 6: usage errors',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e);const f=await proofFile(e);
|
||||
const usageText=(await prun(e,'--help')).err,n=calls(e).length,logs=logLines(e).length,before=fp(e);
|
||||
for(const args of [['activate',C1,'acme','--proof-header','x-console-proof'],['activate',C1,'acme','--proof-file',f],['activate',C1,'acme','--proof-header','','--proof-file',f],
|
||||
['activate',C1,'acme','--no-proof',...pair(f)],['activate',C1,'acme',...pair(f),'--no-proof'],['activate',C1,'acme','--no-proof','--proof-header','x-console-proof'],
|
||||
['rollback','acme',...pair(f)],['rollback','acme','--no-proof'],['rollback','acme','--proof-header','x-console-proof'],['pm-on','acme',...pair(f)],['pm-on','acme','--no-proof'],['pm-on','acme','--proof-file',f],
|
||||
['activate',C1,'acme','--plan','--proof-file',f]]){
|
||||
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
|
||||
}
|
||||
assert.match(usageText,/\[--proof-header NAME --proof-file PATH \| --no-proof\]/);
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 7: a rollback to a copy without proof support drops the pair and says so',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,{copy:A}),f=await proofFile(e);
|
||||
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
const out=await planOnly(e,0,'rollback','acme');
|
||||
assert.deepEqual(proofLines(out),[`proof: dropped; ${A}'s Console does not support --proof-header, so the rollback drops header x-console-proof`]);
|
||||
assert.doesNotMatch(out.find((l)=>l.startsWith('Console: ')),/--proof-/);
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,A);assert.deepEqual(pointer(e).console,base);assert.doesNotMatch(conExec(e),/--proof-/);
|
||||
assert.match(stepText(e),new RegExp(`drop --proof-header x-console-proof --proof-file ${f}; the target Console does not support it\\n`));
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 7: an activate to a copy without proof support refuses proof-target without --no-proof and passes with it',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e),f=await proofFile(e);
|
||||
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
const out=await planOnly(e,3,'activate',A,'acme');
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses proof-target: ${CARRY_MSG(A)}`]);
|
||||
await refused(e,['activate',A,'acme'],'proof-target',CARRY_MSG(A));
|
||||
assert.deepEqual(proofLines(await planOnly(e,0,'activate',A,'acme','--no-proof')),['proof: dropped; --no-proof drops header x-console-proof']);
|
||||
r=await prun(e,'activate',A,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,A);assert.deepEqual(pointer(e).console,base);assert.doesNotMatch(conExec(e),/--proof-/);
|
||||
assert.match(stepText(e),/drop --proof-header x-console-proof [^\n]*; --no-proof was given\n/);
|
||||
await noLeak(e);
|
||||
});
|
||||
});
|
||||
|
||||
+115
-7
@@ -2,7 +2,8 @@
|
||||
# Stack activate and rollback, health-gated (D1, #1659; docs/plans/2026-10-18_stack-release.md).
|
||||
# Moves the bus, the Console and the human CLI from the pointer's copy to another copy together,
|
||||
# behind a health gate, and puts the old stack back inside the same run when the gate fails.
|
||||
# Console tester listener flags follow target support (row 176, #1674).
|
||||
# Console tester listener flags follow target support (row 176, #1674); so does the row 178 proof
|
||||
# pair, which activate sets or drops (row 187, #1685).
|
||||
# Design and tests: agents/darkwing/work/queue-162/BUILD.md.
|
||||
#
|
||||
# Who runs it. Decision 96 (Mos, mos-0716) lets this script stop and start the live units under
|
||||
@@ -27,11 +28,13 @@ BIN="$HOME/.npm-global/bin"
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
CONSOLE_UNIT=mosaic-webui.service
|
||||
TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT=""
|
||||
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'USAGE'
|
||||
usage: stack-release.sh adopt BUSINESS
|
||||
stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N] [--timeout S] [--plan]
|
||||
stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
stack-release.sh status BUSINESS
|
||||
@@ -76,6 +79,9 @@ parse_opts() {
|
||||
--pm) [ "$OP" = activate ] || usage; WANT_PM=1; shift ;;
|
||||
--pm-after-harness) [ "$OP" = activate ] || usage; AFTER_HARNESS=1; shift ;;
|
||||
--tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;
|
||||
--proof-header) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_HEADER="$2"; shift 2 ;;
|
||||
--proof-file) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_FILE="$2"; shift 2 ;;
|
||||
--no-proof) [ "$OP" = activate ] || usage; NO_PROOF=1; shift ;;
|
||||
--migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;;
|
||||
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || usage; RESTORE="$2"; shift 2 ;;
|
||||
--events) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^(0|[1-9][0-9]*)$ ]] || usage; EVENTS="$2"; shift 2 ;;
|
||||
@@ -87,6 +93,9 @@ parse_opts() {
|
||||
[ -z "$TESTER_PORT" ] || [ "$PLAN" = 1 ] || valid_tester_port "$TESTER_PORT" 7332 || usage
|
||||
# --pm-after-harness replaces --pm (row 169 item 1), so the two never go together.
|
||||
[ "$WANT_PM" = 0 ] || [ "$AFTER_HARNESS" = 0 ] || usage
|
||||
# The proof pair goes together, and --no-proof goes alone (row 187).
|
||||
[ "${PROOF_HEADER:+1}" = "${PROOF_FILE:+1}" ] || usage
|
||||
[ "$NO_PROOF" = 0 ] || [ -z "$PROOF_HEADER" ] || usage
|
||||
local d
|
||||
for d in "$COPIES" "$BIN"; do
|
||||
case "$d" in /*) ;; *) usage ;; esac
|
||||
@@ -228,12 +237,16 @@ target_schema() {
|
||||
|
||||
in_chain() { local v; for v in "${T_CHAIN[@]}"; do [ "$v" != "$1" ] || return 0; done; return 1; }
|
||||
|
||||
# The item 3 checks on the target copy, in order: copy, schema, already-active.
|
||||
# The item 3 checks on the target copy, in order: copy, schema, already-active. An activate that
|
||||
# sets a proof pair, or drops the pointer's pair with --no-proof, may name the pointer's copy: it
|
||||
# re-pins the live copy with new Console flags (row 187, Part B's B3 and B-R1).
|
||||
check_target() {
|
||||
copy_exists "$TO" 2>/dev/null || refuse copy-missing "no copy $TO in $COPIES"
|
||||
copy_ok "$TO" 2>/dev/null || refuse copy-unreachable "$TO is not reachable from refactor and origin/refactor"
|
||||
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION and its chain from $COPIES/$TO/packages/bus/src/store.mjs"
|
||||
[ "$TO" != "$P_COPY" ] || refuse already-active "$TO is the pointer's copy"
|
||||
[ "$TO" != "$P_COPY" ] || [ -n "$PROOF_HEADER" ] ||
|
||||
{ [ "$NO_PROOF" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --proof-header "* ]]; } ||
|
||||
refuse already-active "$TO is the pointer's copy"
|
||||
}
|
||||
|
||||
# --- Plug-in kinds (row 169) ---
|
||||
@@ -637,8 +650,8 @@ flags_drift() {
|
||||
[ -z "$d" ] || refuse unit-flags-drift "the units differ from the pointer; settle it first:$d"
|
||||
}
|
||||
|
||||
# Row 178: --proof-header and --proof-file are an opaque pair in P_CONSOLE.
|
||||
# Carry them unchanged on re-pin and rollback; never read the proof value.
|
||||
# Row 178: --proof-header and --proof-file are a pair in P_CONSOLE, which proof_for_target (row
|
||||
# 187) carries unchanged, replaces or drops; never read the proof value.
|
||||
# Resolve desired flags independently of the pointer and effective units, so a hand-edited
|
||||
# unit still refuses drift and a failed attempt can restore exactly the captured stack.
|
||||
# Read the target's own Console, because old copies cannot start with the new flag.
|
||||
@@ -670,6 +683,98 @@ console_for_target() {
|
||||
RUN_CONSOLE=("${without_tester[@]}")
|
||||
DROP_TESTER="--tester-port $inherited"
|
||||
fi
|
||||
proof_for_target
|
||||
}
|
||||
|
||||
# Row 187: the row 178 proof pair in RUN_CONSOLE. A new pair replaces the pointer's and goes last;
|
||||
# --no-proof drops it. With neither, a target Console that takes --proof-header carries the pair
|
||||
# unchanged. One that doesn't drops it on rollback, the way back, and refuses proof-target on
|
||||
# activate, so only --no-proof turns the proof off. Sets PROOF_PLAN (the plan's proof: line) and
|
||||
# PROOF_STEP (a steps log line, or empty).
|
||||
proof_for_target() {
|
||||
local supported=0 had=0 header="" file="" i
|
||||
local -a without_proof=()
|
||||
grep -q -- '--proof-header' "$COPIES/$TO/packages/webui/src/cli.mjs" && supported=1
|
||||
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
|
||||
case "${RUN_CONSOLE[i]}" in
|
||||
--proof-header) had=1; i=$((i + 1)); header="${RUN_CONSOLE[i]:-}" ;;
|
||||
--proof-file) had=1; i=$((i + 1)); file="${RUN_CONSOLE[i]:-}" ;;
|
||||
*) without_proof+=("${RUN_CONSOLE[i]}") ;;
|
||||
esac
|
||||
done
|
||||
PROOF_PLAN="proof: off" PROOF_STEP=""
|
||||
if [ "$NO_PROOF" = 1 ]; then
|
||||
RUN_CONSOLE=("${without_proof[@]}")
|
||||
if [ "$had" = 1 ]; then
|
||||
PROOF_PLAN="proof: dropped; --no-proof drops header $header"
|
||||
PROOF_STEP="drop --proof-header $header --proof-file $file; --no-proof was given"
|
||||
else
|
||||
PROOF_PLAN="proof: off; --no-proof was given and the pointer holds no pair"
|
||||
PROOF_STEP="--no-proof was given and the pointer holds no proof pair, so there is nothing to drop"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$PROOF_HEADER" ]; then
|
||||
# webui-service.sh renders the pair after the preview flags and before --send and
|
||||
# --tester-port, and flags_drift compares the pointer with that argv as it stands.
|
||||
header="$PROOF_HEADER" file="$PROOF_FILE" RUN_CONSOLE=()
|
||||
for ((i = 0; i < ${#without_proof[@]}; i++)); do
|
||||
case "${without_proof[i]}" in --send | --tester-port) break ;; esac
|
||||
RUN_CONSOLE+=("${without_proof[i]}")
|
||||
done
|
||||
RUN_CONSOLE+=(--proof-header "$header" --proof-file "$file" "${without_proof[@]:i}")
|
||||
PROOF_PLAN="proof: on, header $header, file $file"
|
||||
[ "$supported" = 1 ] || check_refuse proof-target "$TO's Console does not support --proof-header"
|
||||
elif [ "$had" = 0 ]; then
|
||||
return 0
|
||||
elif [ "$supported" = 0 ] && [ "$OP" = rollback ]; then
|
||||
RUN_CONSOLE=("${without_proof[@]}")
|
||||
PROOF_PLAN="proof: dropped; $TO's Console does not support --proof-header, so the rollback drops header $header"
|
||||
PROOF_STEP="drop --proof-header $header --proof-file $file; the target Console does not support it"
|
||||
return 0
|
||||
else
|
||||
PROOF_PLAN="proof: carried, header $header"
|
||||
[ "$supported" = 1 ] || check_refuse proof-target "$TO's Console does not support --proof-header, which the pointer's Console flags carry; give --no-proof to drop the pair"
|
||||
fi
|
||||
proof_checks "$header" "$file"
|
||||
}
|
||||
|
||||
# Row 187: proofOptions' own rules (packages/webui/src/serve.mjs), checked before anything stops so
|
||||
# the Console can't fail its start on them. The file gets stat only: this script never opens it,
|
||||
# and no line it writes holds the value.
|
||||
proof_checks() {
|
||||
local header="${1,,}" auth="" seen=" " missing="" f i why
|
||||
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
|
||||
seen+="${RUN_CONSOLE[i]} "
|
||||
[ "${RUN_CONSOLE[i]}" != --auth-header ] || auth="${RUN_CONSOLE[i + 1]:-}"
|
||||
done
|
||||
for f in --public-host --auth-header --allow-user; do
|
||||
[[ "$seen" == *" $f "* ]] || missing+=" $f"
|
||||
done
|
||||
[ -z "$missing" ] || check_refuse proof-needs-preview "the proof pair needs --public-host, --auth-header and --allow-user in the Console flags; the run's flags lack$missing"
|
||||
if ! [[ "$header" =~ ^[a-z0-9][a-z0-9-]{0,63}$ ]] || [ "$header" = host ] || [ "$header" = origin ] ||
|
||||
[ "$header" = x-mosaic-tester ] || [ "$header" = "${auth,,}" ]; then
|
||||
check_refuse proof-header-invalid "--proof-header $1 must be a header name of a-z, 0-9 and - (64 at most, lowercased) other than host, origin, x-mosaic-tester and the --auth-header value"
|
||||
fi
|
||||
why="$(proof_file_problem "$2")"
|
||||
[ -z "$why" ] || check_refuse proof-file-invalid "--proof-file $2 $why"
|
||||
}
|
||||
|
||||
# Why the proof file can't serve, or nothing. lstat, stat and realpath only; the order follows
|
||||
# webui-service.sh render, which refuses the same files.
|
||||
proof_file_problem() {
|
||||
local st uid mode type real copies
|
||||
case "$1" in /*) ;; *) echo "is not an absolute path"; return ;; esac
|
||||
case "$1" in *[!A-Za-z0-9/._+-]*) echo "holds a character outside [A-Za-z0-9/._+-]"; return ;; esac
|
||||
[ ! -L "$1" ] || { echo "is a symlink"; return; }
|
||||
st="$(stat -c '%u:%a:%F' -- "$1" 2>/dev/null)" || { echo "cannot be found"; return; }
|
||||
uid="${st%%:*}" st="${st#*:}"
|
||||
mode="${st%%:*}" type="${st#*:}"
|
||||
case "$type" in "regular file" | "regular empty file") ;; *) echo "is not a regular file"; return ;; esac
|
||||
real="$(realpath -e -- "$1")" && copies="$(realpath -m -- "$COPIES")" || { echo "cannot be resolved"; return; }
|
||||
case "$real" in "$REPO" | "$REPO"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac
|
||||
[ "$mode" = 600 ] || [ "$mode" = 400 ] || { echo "has mode $mode, not 600 or 400"; return; }
|
||||
[ "$uid" = "$(id -u)" ] || echo "is owned by uid $uid, not $(id -u), the uid this script runs as"
|
||||
}
|
||||
|
||||
# The item 3 checks after the pointer, shared by activate and rollback, for target $TO.
|
||||
@@ -794,6 +899,7 @@ plan() {
|
||||
echo "plan: $OP for $BUSINESS"
|
||||
echo "from: $P_COPY"
|
||||
echo "to: $TO"
|
||||
[ "$TO" != "$P_COPY" ] || echo "note: $TO is the pointer's copy; the run re-pins it and only the Console flags change"
|
||||
echo "store schema: $S_SCHEMA ($S_FROM)"
|
||||
echo "target schema: $T_SCHEMA"
|
||||
[ "$S_SCHEMA" = "$P_SCHEMA" ] || [ -n "$S_CHANGED" ] || echo "note: the pointer says $P_SCHEMA; the run reads the stopped store and goes by that"
|
||||
@@ -830,6 +936,7 @@ plan() {
|
||||
[ "$RUN_PENDING" = 0 ] || echo "pm: the bus starts without --pm and the PM stays pending; next: Jason's plugins set of harness, then pm-on"
|
||||
echo "bus: scripts/bus-service.sh install --copy $TO --copies $COPIES$([ "$RUN_PM" = 0 ] || echo ' --pm') --claude $P_CLAUDE"
|
||||
[ -z "$DROP_TESTER" ] || echo "drop $DROP_TESTER; the target Console does not support it"
|
||||
echo "$PROOF_PLAN"
|
||||
echo "Console: scripts/webui-service.sh install --copy $TO --copies $COPIES${RUN_CONSOLE[*]:+ ${RUN_CONSOLE[*]}}"
|
||||
echo "CLI: scripts/stack-copy.sh wrapper $TO --copies $COPIES --bin $BIN"
|
||||
local r
|
||||
@@ -842,6 +949,7 @@ run_steps() {
|
||||
log_line result started
|
||||
capture || refuse capture-failed "could not write the rollback set $RS; nothing was stopped"
|
||||
[ -z "$DROP_TESTER" ] || say "drop $DROP_TESTER; the target Console does not support it"
|
||||
[ -z "$PROOF_STEP" ] || say "$PROOF_STEP"
|
||||
trap 'after_fail signal' INT TERM HUP
|
||||
stop_both || rollback_in_run stop-timeout
|
||||
if [ -e "$STORE" ]; then
|
||||
@@ -1070,7 +1178,7 @@ STORE="$DATA/bus/bus.sqlite"
|
||||
BUS_UNIT="mosaic-bus@$BUSINESS.service"
|
||||
FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL=""
|
||||
STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED=""
|
||||
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PLAN_REFUSE=()
|
||||
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" PLAN_REFUSE=()
|
||||
case "$OP" in
|
||||
adopt) do_adopt ;;
|
||||
activate) do_activate ;;
|
||||
|
||||
Reference in New Issue
Block a user