Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
270 lines
18 KiB
Diff
270 lines
18 KiB
Diff
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
|
|
index ad1a24add..49fa5d1e7 100644
|
|
--- a/docs/TOOLS.md
|
|
+++ b/docs/TOOLS.md
|
|
@@ -387,6 +387,12 @@ one final line with `result` `ok`, `refused`, `gate-failed`,
|
|
failure that rolled back exits 1, a failed rollback exits 2, usage exits 4. Only one run at a time holds
|
|
`state/stack-release.lock` (`run-locked`).
|
|
|
|
+An activation id is the UTC second and the target copy's first 12 characters,
|
|
+`20261020T013708Z-89af38a83afa`. A second run in the same second adds `-1`,
|
|
+then `-2` up to `-9`, the first one with no rollback set, backup or
|
|
+post-attempt store yet, so ids still sort by time. With all ten taken the run
|
|
+refuses `capture-failed` before it stops anything. Adopt uses the same rule.
|
|
+
|
|
1. Once per host, `adopt BUSINESS` records the running stack as the pointer:
|
|
the copy `stack-copy.sh check` reports, the bus unit's `--pm` and
|
|
`MOSAIC_CLAUDE_BIN`, the Console's flags without `--checkout`, and the
|
|
@@ -421,11 +427,13 @@ failure that rolled back exits 1, a failed rollback exits 2, usage exits 4. Only
|
|
this start. A pass writes the pointer.
|
|
6. A gate failure puts back the saved units and wrapper, restores the backup
|
|
when a migration ran, keeps the store it replaced as
|
|
- `bus.post-attempt-<stamp>`, starts the old copy and gates it. If that gate
|
|
+ `bus.post-attempt-<stamp>`, where the stamp is the activation's second and
|
|
+ its suffix if it has one, starts the old copy and gates it. If that gate
|
|
fails too, both units stay stopped, the log says `rollback-failed`, and
|
|
Jason or Mos settles it from the rollback set the message names.
|
|
7. `rollback BUSINESS` returns to the copy before the pointer's, taken from
|
|
- the log. When the schemas differ it needs `--restore ACTIVATION`; events
|
|
+ the log. When the schemas differ it needs `--restore ACTIVATION`, with or
|
|
+ without a `-1` to `-9` suffix; events
|
|
written after that backup need `--events N` with the count it prints, and
|
|
Jason decides first. It refuses `business-unreadable` and
|
|
`unknown-kind-for-target` like `activate`. For the second, set each kind
|
|
@@ -834,7 +842,10 @@ unit write for the authorized operator, with no start/restart verb.
|
|
`stack-release.sh activate ID BUSINESS --tester-port N` replaces or adds this
|
|
Console flag in both the installed unit and the pointer. It checks the target
|
|
copy's own Console before stopping anything; an explicit unsupported request
|
|
-refuses `tester-port-target`. Without an explicit flag, activate and rollback
|
|
+refuses `tester-port-target`. A port outside 1024 to 65535, 7332 or the
|
|
+Console's `--port` is a usage error (exit 4). Under `--plan` both are listed
|
|
+with the plan's other refusals instead, the bad port as `tester-port-invalid`,
|
|
+and the plan exits 3. Without an explicit flag, activate and rollback
|
|
keep it between supporting copies, or drop it when the target predates support.
|
|
The plan prints the final Console flags and any dropped `--tester-port N`;
|
|
the steps log records a drop too. `pm-on` keeps the pointer's Console flags.
|
|
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
|
|
index 56d8eeb55..c7d9478c9 100644
|
|
--- a/packages/cli/tests/stack-release.test.mjs
|
|
+++ b/packages/cli/tests/stack-release.test.mjs
|
|
@@ -1095,3 +1095,105 @@ test('T42 tester-port pm-on preserves pointer Console flags',async(t)=>{
|
|
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`);
|
|
+ });
|
|
+});
|
|
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
|
|
index cc59bcb12..8811a5b9e 100644
|
|
--- a/packages/webui/tests/service.test.mjs
|
|
+++ b/packages/webui/tests/service.test.mjs
|
|
@@ -170,6 +170,19 @@ test('tester service invalid ports refuse render and install with exit 4 and lea
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
|
});
|
|
|
|
+// Row 180 (Dewey's row 176 N2): the length counts as written, before the zeros go, as serve
|
|
+// counts it. 007333 is six digits and refuses; 07333 is five and renders.
|
|
+test('tester service counts the digits as written before it strips leading zeros', async (t) => {
|
|
+ const dir = tmp(t);
|
|
+ const six = await run(dir, ['render', '--tester-port', '007333']);
|
|
+ assert.equal(six.status, 4, six.stderr);
|
|
+ assert.equal(six.stdout, '');
|
|
+ assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');
|
|
+ const five = await run(dir, ['render', '--tester-port', '07333']);
|
|
+ assert.equal(five.status, 0, five.stderr);
|
|
+ assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));
|
|
+});
|
|
+
|
|
test('tester service install and status show the tester port without changing send reporting', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
diff --git a/scripts/stack-release.sh b/scripts/stack-release.sh
|
|
index adcb68477..f70295e99 100755
|
|
--- a/scripts/stack-release.sh
|
|
+++ b/scripts/stack-release.sh
|
|
@@ -73,14 +73,16 @@ parse_opts() {
|
|
--plan) [ "$OP" = activate ] || [ "$OP" = rollback ] || usage; PLAN=1; shift ;;
|
|
--pm) [ "$OP" = activate ] || usage; WANT_PM=1; shift ;;
|
|
--pm-after-harness) [ "$OP" = activate ] || usage; AFTER_HARNESS=1; shift ;;
|
|
- --tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] || usage; TESTER_PORT="$2"; valid_tester_port "$TESTER_PORT" 7332 || usage; shift 2 ;;
|
|
+ --tester-port) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; TESTER_PORT="$2"; shift 2 ;;
|
|
--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}$ ]] || usage; RESTORE="$2"; shift 2 ;;
|
|
+ --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 ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
[ -z "$EVENTS" ] || [ -n "$RESTORE" ] || usage
|
|
+ # A bad tester port is a usage error; --plan lists it as tester-port-invalid instead (row 180).
|
|
+ [ -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
|
|
local d
|
|
@@ -633,8 +635,14 @@ console_for_target() {
|
|
RUN_CONSOLE=("${P_CONSOLE[@]}")
|
|
DROP_TESTER=""
|
|
if [ -n "$TESTER_PORT" ]; then
|
|
- valid_tester_port "$TESTER_PORT" "$main" || usage
|
|
- [ "$supported" = 1 ] || refuse tester-port-target "$TO's Console does not support --tester-port"
|
|
+ # --plan lists both with its other refusals (row 180); outside it they end the run.
|
|
+ if ! valid_tester_port "$TESTER_PORT" "$main"; then
|
|
+ [ "$PLAN" = 1 ] || usage
|
|
+ local other=""
|
|
+ [ "$main" = 7332 ] || other=" and the Console's --port $main"
|
|
+ check_refuse tester-port-invalid "--tester-port $TESTER_PORT is not a whole port from 1024 to 65535 other than 7332$other"
|
|
+ fi
|
|
+ [ "$supported" = 1 ] || check_refuse tester-port-target "$TO's Console does not support --tester-port"
|
|
RUN_CONSOLE=("${without_tester[@]}" --tester-port "$TESTER_PORT")
|
|
elif [ -n "$inherited" ] && [ "$supported" = 0 ]; then
|
|
RUN_CONSOLE=("${without_tester[@]}")
|
|
@@ -659,10 +667,25 @@ pointer_for_business() {
|
|
[ "$P_BUSINESS" = "$BUSINESS" ] || refuse business-mismatch "the pointer is for $P_BUSINESS"
|
|
}
|
|
|
|
+# The activation id is the second and the target's 12-character prefix. A second run in the same
|
|
+# second adds -1 to -9, so ids still sort by time and an id without one is still whole (row 180).
|
|
+# Callers hold the lock. Sets ACT and POST from one free suffix: no rollback set, backup or
|
|
+# post-attempt store with that name.
|
|
+next_activation() {
|
|
+ local second s
|
|
+ second="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
+ for s in "" -1 -2 -3 -4 -5 -6 -7 -8 -9; do
|
|
+ ACT="$second-${TO:0:12}$s" POST="$DATA/bus.post-attempt-$second$s"
|
|
+ [ -e "$STATE/stack-rollback/$ACT" ] || [ -e "$DATA/backups/stack/$ACT" ] || [ -e "$POST" ] || return 0
|
|
+ done
|
|
+ # Every name is another run's, so the refused line names none and closes no started run.
|
|
+ ACT="" POST=""
|
|
+ refuse capture-failed "every activation id for $second-${TO:0:12} is taken; nothing was stopped"
|
|
+}
|
|
+
|
|
new_activation() {
|
|
- ACT="$(date -u +%Y%m%dT%H%M%SZ)-${TO:0:12}"
|
|
+ next_activation
|
|
RS="$STATE/stack-rollback/$ACT"
|
|
- POST="$DATA/bus.post-attempt-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
}
|
|
|
|
# --- Verbs ---
|
|
@@ -677,7 +700,7 @@ do_adopt() {
|
|
read_units || refuse check-failed "cannot read $BUS_UNIT or $CONSOLE_UNIT"
|
|
[ -n "$U_CLAUDE" ] || refuse no-claude-pin "$BUS_UNIT has no MOSAIC_CLAUDE_BIN"
|
|
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION from $COPIES/$TO/packages/bus/src/store.mjs"
|
|
- ACT="$(date -u +%Y%m%dT%H%M%SZ)-${TO:0:12}"
|
|
+ next_activation
|
|
write_pointer "$TO" "$U_PM" 0 "$U_CLAUDE" "$T_SCHEMA" "${U_CONSOLE[@]}"
|
|
log_line result ok
|
|
echo "adopted $TO for $BUSINESS (pm $U_PM, schema $T_SCHEMA)"
|
|
diff --git a/scripts/webui-service.sh b/scripts/webui-service.sh
|
|
index b0782dd34..71db7e5c1 100755
|
|
--- a/scripts/webui-service.sh
|
|
+++ b/scripts/webui-service.sh
|
|
@@ -122,6 +122,8 @@ tester_port_usage() {
|
|
|
|
tester_port_ok() {
|
|
case "$TESTER_PORT" in ''|*[!0-9]*) tester_port_usage ;; esac
|
|
+ # At most five digits as written, before the zeros go, as serve and stack-release.sh count them.
|
|
+ [ "${#TESTER_PORT}" -le 5 ] || tester_port_usage
|
|
local tester_port="$TESTER_PORT"
|
|
# Compare decimal values, including values written with leading zeros.
|
|
while [ "${tester_port#0}" != "$tester_port" ]; do tester_port="${tester_port#0}"; done
|