diff --git a/docs/TOOLS.md b/docs/TOOLS.md index e0447341c..3a9de2aa7 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -404,9 +404,12 @@ refuses `capture-failed` before it stops anything. Adopt uses the same rule. full id. 3. Run `activate ID BUSINESS --plan` and take the output to Mos. It names the store's schema and the target's, any migration with `--migrate` and the - decision 96 condition 2 line (Jason present), the commits the run adds, - and the three install commands. It refuses `copies-unrelated` when neither - copy is an ancestor of the other. + decision 96 condition 2 line (Jason present), the backup, the commits the + run adds, and the three install commands. The `backup:` line gives the + directory the run backs the stopped store up to, or why it takes none: + there is no store file, or the run rolls back with `store-newer` or + `migration-needs-flag` before the backup. It refuses `copies-unrelated` + when neither copy is an ancestor of the other. 4. Mos removes any bus drop-in that overrides ExecStart or WorkingDirectory; `activate` refuses `drop-in-override` and names the file. 5. With Mos's go, `activate ID BUSINESS`, plus `--migrate` when the plan diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs index e85968006..a2c0e720c 100644 --- a/packages/cli/tests/stack-release.test.mjs +++ b/packages/cli/tests/stack-release.test.mjs @@ -380,6 +380,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// 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}`]; // --- The T table --- @@ -643,9 +644,9 @@ test('T26 --plan lists the commits an activate adds, after the migration lines', 'migration: 3g to 3h, after a backup of the stopped store','migration steps: 3g to 3h',`needs: --migrate (${mig})`,'needs: Jason present for this run (decision 96 condition 2)']; const adds=commitLines(`${B}..${C}`); assert.equal(adds.length,2);assert.match(adds[0],/^ {2}[0-9a-f]{7,} row 904: C$/);assert.match(adds[1],/^ {2}[0-9a-f]{7,} row 903: C1$/); - for(const [flags,mig] of [[[],'not given; the run would roll back with migration-needs-flag'],[['--migrate'],'given']]){ + for(const [flags,mig,backup] of [[[],'not given; the run would roll back with migration-needs-flag',planNoBackup],[['--migrate'],'given',planBackup(e)]]){ const r=await run(e,'activate',C,'acme','--plan',...flags);assert.equal(r.code,0,r.err); - assert.equal(r.out,[...head(mig),planBackup(e),'commits: the run adds 2 (newest first, at most 200):',...adds,...planTail(e,C),''].join('\n')); + assert.equal(r.out,[...head(mig),backup,'commits: the run adds 2 (newest first, at most 200):',...adds,...planTail(e,C),''].join('\n')); } const same=await run(e,'activate',C1,'acme','--plan');assert.equal(same.code,0,same.err); assert.ok(same.out.includes(`target schema: 3g\nmigration: none; the store is at the target's version\n${planBackup(e)}\ncommits: the run adds 1 (newest first, at most 200):\n`),same.out); @@ -1373,6 +1374,31 @@ test('T51 row 185 test 5: an activate with no store file takes no backup',async( const l=logLines(e).at(-1);assert.deepEqual([l.result,l.backup],['ok',null]); }); +// Row 186 (#1684): the activate plan prints the backup line only when the run reaches backup_store. +// Each case plans, checks nothing was written, then runs and checks a backup exists exactly when the plan said so. +test('T52 row 186: the activate plan\'s backup line says what the run will do',async(t)=>{ + const newer='migration: none possible; 3z is not in 3h\'s migration chain, so the run would roll back with store-newer'; + const cases=[ + ['same schema','3g',C1,[],'migration: none; the store is at the target\'s version',true,0], + ['same schema with --migrate','3g',C1,['--migrate'],'migration: none; the store is at the target\'s version',true,0], + ['in chain with --migrate','3g',C,['--migrate'],'needs: --migrate (given)',true,0], + ['in chain without --migrate','3g',C,[],'needs: --migrate (not given; the run would roll back with migration-needs-flag)',false,1], + ['outside the chain','3z',C,[],newer,false,1], + ['outside the chain with --migrate','3z',C,['--migrate'],newer,false,1], + ]; + for(const [name,store,to,flags,why,backup,code] of cases)await t.test(name,async(t)=>{ + const e=await mk(t);await seed(e,{store}); + const before=fp(e),data=tree(e.data),n=calls(e).length,logs=logLines(e).length; + const p=await run(e,'activate',to,'acme','--plan',...flags);assert.equal(p.code,0,p.err);assert.equal(p.err,''); + const out=p.out.split('\n');assert.ok(out.includes(why),p.out); + assert.deepEqual(out.filter((x)=>x.startsWith('backup')),[backup?planBackup(e):planNoBackup]); + assert.deepEqual(fp(e),before);assert.deepEqual(tree(e.data),data);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs); + assert.ok(!existsSync(join(e.data,'backups'))); + const r=await run(e,'activate',to,'acme',...flags,'--timeout','3');assert.equal(r.code,code,r.out+r.err); + assert.equal(existsSync(join(e.data,'backups','stack')),backup);assert.equal(logLines(e).at(-1).backup??null,backup?logLines(e).at(-1).activation:null); + }); +}); + // Row 178: proof metadata uses temp fixture files, never the installed slot. async function seedProof(e) { await seed(e); diff --git a/scripts/stack-release.sh b/scripts/stack-release.sh index f8051d81c..5779b64f7 100755 --- a/scripts/stack-release.sh +++ b/scripts/stack-release.sh @@ -797,6 +797,9 @@ plan() { 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" + # Row 186: set when run_steps would roll back with store-newer or migration-needs-flag, which + # come before backup_store. A read the bus stopped under judges nothing, so it keeps the backup line. + local early="" if [ -n "$S_CHANGED" ]; then : # no migration or restore line from a read the bus stopped under elif [ "$S_SCHEMA" = "$T_SCHEMA" ]; then @@ -806,17 +809,21 @@ plan() { echo "migration steps: $(plan_steps)" echo "needs: --migrate$([ "$MIGRATE" = 1 ] && echo ' (given)' || echo ' (not given; the run would roll back with migration-needs-flag)')" echo "needs: Jason present for this run (decision 96 condition 2)" + [ "$MIGRATE" = 1 ] || early=1 elif [ "$OP" = activate ]; then echo "migration: none possible; $S_SCHEMA is not in $T_SCHEMA's migration chain, so the run would roll back with store-newer" + early=1 else echo "restore: the store must go back to $T_SCHEMA; needs --restore $(from_log backup || echo ACTIVATION)" fi if [ "$OP" != activate ]; then : # rollback takes no backup of its own - elif [ -e "$STORE" ]; then - echo "backup: the run backs up the stopped store to $DATA/backups/stack// before any migration" - else + elif [ ! -e "$STORE" ]; then echo "backup: none; there is no store file" + elif [ -n "$early" ]; then + echo "backup: none; the run rolls back before the backup" + else + echo "backup: the run backs up the stopped store to $DATA/backups/stack// before any migration" fi plan_commits [ "$P_PENDING" = 0 ] || echo "pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM"