Blobs of Darkwing's round 2da2da2f6con origin/darkwing/row232-r2 (base8fede5fc6), the 9 manifest paths, manifest 7af3c943. Round 2 is a rebase-only round over row 230 (c8bc5cf4c): packages/cli/README.md is a clean 3-way merge with 232's hunk unchanged, and the other 8 paths keep the round 1 hashes (manifest 7a3b4ea1). Filbert approve round 1 (comment 29829) and round 2 (comment 29865). Under --units user every command line and every byte of output is the base's. Under external the script reads root's system units, starts and stops none, stages a copy behind AWAITING-UNITS, and confirm gates root's start on the bus unit's ActiveEnterTimestamp and launches list. Mutants 17 of 17 killed (BUILD.md). Darkwing's landing gate onda2da2f6c, 03:07:15Z to 03:17:48Z: packages/cli 593 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node tests in the queue suite). Refactor moved to0444acd08since, three commits touching no manifest path. No live change: the stack runs from copy4429a104f, and --units user stays the default.
288 lines
19 KiB
Markdown
288 lines
19 KiB
Markdown
# Row 232 (I1): stack-release external units and bundle copies
|
|
|
|
Brief: `docs/plans/2026-10-20_installer-rows.md` section I1, blob `f35e7e57daee` (commit ad6f0be7a). Gitea
|
|
#1734, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
|
|
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66 (approved in duckworth-0055). Where the brief and
|
|
r6 differ, the brief wins. sage-1561 ruled where the stack user's `stack-release.sh` runs: from its own
|
|
clone of the release bundle, so `check_target`, the plan's commit list and status drift are unchanged and
|
|
nothing adds `safe.directory`.
|
|
|
|
## Files
|
|
|
|
Base: cb5fdb064 (row 199's landing, on top of row 200's 9e1085816). Branch `darkwing/row232-r1`. The first
|
|
build, 0af0c73c6 on `darkwing/row232`, sat on 00d4cc1f6 (row 190); row 200 landed first, so this one is
|
|
rebased onto it (see D9 and D10).
|
|
|
|
- `scripts/stack-release.sh`: `--units user|external`, the `sc` and `jctl` backend, `confirm`, `revert`,
|
|
`gate --since`, `rollback --to`, `--plan --expect-from`, the external gate (start floor, PM check from
|
|
`launches list`), `external-pm`, and the `awaiting units:` status line.
|
|
- `scripts/stack-copy.sh`: `check --units external`, `copy --bundle FILE SHA`, `wrapper ID --create`.
|
|
- `packages/cli/tests/stack-release-external.test.mjs` (new, 25 tests).
|
|
- `packages/cli/tests/stack-release-fixture.mjs` (new): the helpers `stack-release.test.mjs` defined, moved
|
|
so both files share them. See deviation D1.
|
|
- `packages/cli/tests/stack-release.test.mjs`: the helpers leave and one import line comes in. The 74 tests
|
|
have the same names and the same bodies as on the base.
|
|
- `packages/cli/tests/stack-copy.test.mjs`: three new `row 232:` tests and a wider `node:fs` import.
|
|
- `docs/TOOLS.md`: the stack-copy usage block and three paragraphs, the stack-release usage block, and a new
|
|
subsection "Root-owned units (`--units external`, row 232)".
|
|
- `packages/cli/README.md`: one paragraph.
|
|
- This file.
|
|
|
|
The worktree's `node_modules` symlink is not part of the candidate.
|
|
|
|
## User mode is unchanged
|
|
|
|
Every `systemctl` call goes through `sc` and every `journalctl` call through `jctl`. Under `user` they
|
|
are `systemctl --user "$@"` and `journalctl --user "$@"`. Under `external`, `sc` allows only `show`,
|
|
`is-active` and `list-units`, and `jctl` returns 1, so an external run can't start, stop, install or read
|
|
the journal even through a path the tests miss.
|
|
|
|
Two tests hold the brief's "every command line and every byte of output":
|
|
|
|
- `user mode: every systemctl call carries --user, and activate, rollback and check make the base's
|
|
calls`. A fixture repo gets the base's two scripts from `git show cb5fdb064:`. The same activate,
|
|
rollback and `stack-copy.sh check` run from both trees, and the recorded argv (with the fixture root and
|
|
port replaced by placeholders) must match line for line. Every call carries `--user`.
|
|
- `user mode: a default activate --plan prints the base's bytes`, for four plans.
|
|
|
|
The base output is produced live from the base blobs, not stored as a fixture file. See deviation D3.
|
|
|
|
## r6 section 7, test by test
|
|
|
|
All in `stack-release-external.test.mjs` unless named.
|
|
|
|
| r6 :742-812 item | Test |
|
|
|---|---|
|
|
| `units-running` (N4), store, log and TAKEN left | `units-running (N4) refuses while a unit is active...`; also checks that `failed` counts as down and that the Console is checked first |
|
|
| activate leaves the pointer, writes `AWAITING-UNITS` and a backup | `activate stages: ...`, which also checks the set's file list, `UNITS.sha256`, no `--user`, no journal call and `status` |
|
|
| `open-activation` with zero, two, the wrong copy, one not open | `confirm and revert refuse open-activation ...` |
|
|
| `confirm` polls: 3 s fails at `--timeout 1`, passes at `--timeout 10` | `confirm polls: bus status good after 3 s fails ...` and `confirm polls to a pass ...` |
|
|
| `stale-start` floor with `started` at .900 | `stale-start: @1792540799 fails ...` and `stale-start is a gate code ...` |
|
|
| `confirm` pass writes pointer, ok, removes marker; timeout writes `FAILED` and exits 1 | the two polling tests |
|
|
| `activation-failed` when `FAILED` exists | `confirm polls: ... FAILED then refuses activation-failed` |
|
|
| lines carry the started op, from, to, activation and backup | `sameAsStarted` in every confirm test, and sequence 7 |
|
|
| `gate` (N2) with and without `--since` | `gate (N2, A2f): ...` and the usage test |
|
|
| `--plan --expect-from` (N5), activate and `rollback --to` | sequence 7 |
|
|
| A3 order: capture-failed, marker-failed, wrapper failure after migrate | `A3 order: ...` and sequence 2 |
|
|
| refusals log no activation; `interrupted` unchanged | `open-activation` test and sequence 5 |
|
|
| PM check: one, none, two, early, not live, other business, other instance; no journal call | `the external PM check reads launches list ...` (modes '', coder, none, twolive, early, dead, other) and `journalCalls` in every external test |
|
|
| `external-pm` | `pm-on and activate --pm-after-harness refuse external-pm` |
|
|
| `revert` restores TAKEN only after a migration | `revert puts the wrapper back and leaves the store ...` and sequences 2 and 8 |
|
|
| `confirm --rollback` per cause, `unrecorded`, `rollback-failed`, mismatch | sequences 1, 2, 3, 5 and 8, and `confirm --rollback refuses rollback-mismatch, and logs rollback-failed ...` |
|
|
| sequences 1-8 | `sequence 1:` to `sequence 8:`, each ending with the `closed` check (one started and one result line, empty `interrupted`, no marker or `FAILED`, a following `--plan` that doesn't refuse `interrupted-run`) |
|
|
| `rollback --to` refuses not-previous; `rollback --restore` finds an external backup | sequence 7, and `confirm --rollback after a --restore swap ...` |
|
|
| `gate` writes nothing | the `gate (N2, A2f)` test and sequence 4 compare the log, the pointer and the state tree |
|
|
| `confirm`, `revert`, `gate`, `--to` refuse without `--units external` | `confirm, revert, gate, --to and --expect-from need --units external ...` |
|
|
| default path: every call carries `--user`; default `--plan` bytes | the two user-mode tests above |
|
|
|
|
The brief's additions, in `stack-copy.test.mjs`:
|
|
|
|
- `row 232: copy --bundle copies a SHA the bundle's refactor reaches, byte-identical to copy SHA, and
|
|
refuses the rest`: a short SHA, a missing file, a symlink, a bundle that fails `git bundle verify`, a
|
|
bundle with no `refs/heads/refactor`, a SHA not in the bundle, and a SHA the bundle carries on another
|
|
branch that refactor doesn't reach. The bundle copy's tree matches `copy SHA`'s byte for byte, and the
|
|
`release:` and `bundle sha256:` lines are checked.
|
|
- `row 232: wrapper --create writes the first wrapper only, the same bytes wrapper ID writes, and refuses
|
|
over any file`, a symlink included. Without `--create` a missing wrapper still refuses.
|
|
- `row 232: check --units external reads the system units, the default reads the user units`.
|
|
|
|
## Mutants
|
|
|
|
`mutants.sh` in my scratch (not committed, it edits the worktree), 17 mutants: r6's re-pin list (:817-838)
|
|
minus the three `case1-repin.sh` ones, plus the brief's two. Each one applies exact replacements after a
|
|
checked backup, runs `bash -n`, runs its named killers, restores the file and checks its sha256. Two
|
|
controls run first with no change: the whole external file, and stack-copy's `row 232:` tests.
|
|
|
|
| Mutant | Killers |
|
|
|---|---|
|
|
| m01 `confirm` writes the pointer on a gate failure | `confirm polls: bus status` |
|
|
| m02 `gate_once` in place of `gate` | `confirm polls to a pass`, `stale-start is a gate code` |
|
|
| m03 PM check counts any instance | `external PM check` |
|
|
| m04 PM check drops the `startedAt` bound | `external PM check` |
|
|
| m05 `confirm` takes the newest `AWAITING-UNITS` | `open-activation` |
|
|
| m06 `rollback --to` skips the comparison | `sequence 7:` |
|
|
| m07 the default path drops `--user` from one call (`is-active`) | `user mode: every systemctl` |
|
|
| m08 the timeout writes r4's interim `rollback pending` line | `confirm polls: bus status`, `sequence 1:`, `sequence 5:` |
|
|
| m09 `AWAITING-UNITS` last, after the wrapper | `sequence 2:` |
|
|
| m10 a refusal names the open activation | `sequence 5:`, `open-activation` |
|
|
| m11 `stale-start` is a refusal | `stale-start is a gate code` |
|
|
| m12 `revert` restores without `MIGRATING` | `revert puts the wrapper back` |
|
|
| m13 `confirm` logs op `confirm` | `sequence 7:`, `confirm polls to a pass` |
|
|
| m14 `MIGRATING` after the migration | `sequence 8:` |
|
|
| m15 `gate` takes its floor from the log without `--since` | `gate (N2` |
|
|
| m16 `copy --bundle` skips the reachability check | stack-copy `row 232:` |
|
|
| m17 `wrapper --create` overwrites | stack-copy `row 232:` |
|
|
|
|
## Deviations and choices
|
|
|
|
D1. The shared helpers moved to a new file, `packages/cli/tests/stack-release-fixture.mjs`. The brief's
|
|
files list names `stack-release.test.mjs` "where a shared helper moves" but not a destination. Importing
|
|
from the other test file would run its 74 tests twice, so the helpers need a module of their own.
|
|
|
|
D2. The fixture's T17 check (what a run may never remove) now exempts a rollback set's `AWAITING-UNITS` and
|
|
`FAILED`. `confirm` removes both when it closes the activation (r6 A5 and R4). User mode writes neither, so
|
|
user-mode runs check exactly what they did.
|
|
|
|
D3. The user-mode argv and `--plan` tests read the base's scripts with `git show cb5fdb064:` rather than
|
|
storing their output as a fixture file. A later row that changes user-mode output on purpose moves the
|
|
`BASE` constant in the test to its own base, or the test fails. I chose this over a stored fixture because
|
|
a stored copy can drift from the base without anyone noticing.
|
|
|
|
D4. `marker-failed` is reached by making `state/stack-rollback` unwritable. r6 says "an existing `$RS`", but
|
|
`next_activation` never picks a taken name, so no test can make the run meet an existing `$RS`. The
|
|
exclusive `mkdir` is still what refuses.
|
|
|
|
D5. The stale-start polling case turns the enter time to `@1792540800`, not r6's `@1792540805`, with the
|
|
started line at 1792540800.900. That also checks that the floor is the whole second, so .900 doesn't push it
|
|
to 1792540801.
|
|
|
|
D6. No fake `sudo` or `case2-unit.sh`. r6's sequences ran under them because r6 had `case1-repin.sh` drive
|
|
the steps, and that script is I3's. Here the test plays root: it stops and starts the fake system units and
|
|
installs with the existing `bus-service.sh` and `webui-service.sh` stand-ins, between the real
|
|
`stack-release.sh` calls, on one log.
|
|
|
|
D7. The fake marker in the `open-activation` test is named `20000101T000000Z-<C1 prefix>` so it sorts
|
|
before the real one. A marker that sorted after it would leave m05 (newest marker) alive.
|
|
|
|
D8. m10 sets `ACT` inside `open_activation`, where r6's wording ("ACT set before the checks") lands in this
|
|
code. `ACT` is set only after every check in `confirm`.
|
|
|
|
D9. Row 200's `--sso` is a usage error under `--units external`, with the proof flags, `--no-proof` and
|
|
`--no-send`. It is a Console flag, and root's Console unit carries the Console flags; the script under
|
|
external renders no Console line and never calls `console_for_target`, so `sso_checks` doesn't run there
|
|
either. The usage text, the TOOLS.md bullet and the external usage test name it.
|
|
|
|
D10. Row 200's `config_checks` runs under `--units external` too, after `plugin_checks`, in both the
|
|
`--plan` and run paths of `activate` and `rollback --to`. It checks the target's loader against the stack
|
|
user's `config.json`, which the bus on a VM still reads, so a `console` key the target refuses would break
|
|
root's bus start the same way. The cost is one more refusal (`config-console-key`) an external run can
|
|
print; I took it over a bus that fails after root's start. The rebase changed no user-mode line: the
|
|
conflicts were the `SSO=0` globals line, the `plan` body (row 200's `SSO_PLAN` line sits in the user
|
|
branch) and the two `--plan` lines that gained `config_checks`. In the tests, row 200's fixture lines
|
|
(the loaders, `sso-config.mjs` and the `--sso` cli.mjs line for C1 and C) moved into
|
|
`stack-release-fixture.mjs` with the rest of the copy setup, and the `ALGS` import stays in
|
|
`stack-release.test.mjs`, which T57 uses.
|
|
|
|
## duckworth-0055's notes on r6 (brief line 169)
|
|
|
|
Duckworth's r6 approval (REVIEW-r5.md at 23f5520b5) carried two notes.
|
|
|
|
- N1, A2's stop timeout under `gate --since`. A unit that never stopped keeps its old `ActiveEnterTimestamp`,
|
|
so `gate --since` fails it with `stale-start`, and the run ends at exit 2 with root stopping the units.
|
|
That is intended: a unit that won't stop is a fair reason to stop. In I1 this is the gate code and its
|
|
polling (`stale-start: @1792540799 fails ...`, `gate (N2, A2f)`, m11). The run sheet's A2f line and the
|
|
re-pin test that points "exits 1 with OLD's gate passing" at an A3 exit 3 belong to I3, which owns
|
|
`scripts/stack-upgrade.sh` and `docs/guides/host-run-sheet.md`. I'll name both in the I3 build.
|
|
- N2, the claude path under `/opt/mosaic-stack` (gosalyn-0058, sage-1522). I1 doesn't touch it. Under
|
|
`--units external` the script checks no claude pin of its own (`no-claude-pin` applies only to user
|
|
mode), because root's unit carries it. The `--claude` value, the managed-settings reasoning, F6 and the
|
|
run sheet's claude step go to I2 and I3.
|
|
|
|
## Run environment
|
|
|
|
Worktree `/mnt/storage/scratch/darkwing-r232`, `TMPDIR=/mnt/storage/scratch/tmp/r232`, node from PATH.
|
|
One external file run takes about 94 s.
|
|
|
|
## Results
|
|
|
|
Singles on the rebased candidate, before the gate:
|
|
|
|
| File | Tests | Pass | Fail |
|
|
|---|---|---|---|
|
|
| `packages/cli/tests/stack-release-external.test.mjs` | 25 | 25 | 0 |
|
|
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 16 | 0 |
|
|
| `packages/cli/tests/stack-release.test.mjs` (row 200's T57 included) | 192 | 192 | 0 |
|
|
| `packages/cli/tests/stack-instances.test.mjs` | 23 | 23 | 0 |
|
|
| `packages/webui/tests/pinned-copy.test.mjs` | 10 | 10 | 0 |
|
|
| `packages/cli/tests/channel-recovery.test.mjs` (imports `channel-fixture.mjs`, which names the scripts) | 1 | 1 | 0 |
|
|
| `packages/cli/tests/host.test.mjs` (reads `packages/cli/README.md`) | 42 | 42 | 0 |
|
|
|
|
These are every test file that reads `stack-release.sh` or `stack-copy.sh` (a grep of `packages/*/tests` and
|
|
`scripts/`). None of them pins a sha256 of a changed file. `packages/cli/tests/host.test.mjs` reads
|
|
`packages/cli/README.md` for a unit file name; this row only adds a paragraph there. The grep and the runs
|
|
were repeated on the rebased tree on 2026-10-21; the reader set is the same. A dry run of `mutants.sh`
|
|
matches each of the 17 replacements once on the rebased scripts.
|
|
|
|
### Gate, 2026-10-21, Sage's slot (sage-1618)
|
|
|
|
Candidate a0ffabc3f on `darkwing/row232-r1`, run by `~/darkwing-scratch/r232/gate.sh`: the cli suite, then
|
|
every `scripts/test-*.sh`, one after another, stopping at the first failure. busy.sh passed at 01:07:49Z
|
|
after Launchpad's single `stack-release.test.mjs` run ended. The gate stopped at the first one, so the
|
|
chain waited for that PID to exit and checked again. The EXIT trap removed the compose network; no
|
|
container was left.
|
|
|
|
| Step | Time (UTC) | Result |
|
|
|---|---|---|
|
|
| `packages/cli`, `node --test tests/*.test.mjs` | 01:07:50 to 01:17:53 | 561 pass, 0 fail, 0 skipped |
|
|
| `scripts/test-auth.sh` | 01:17:53 to 01:17:54 | 15 passed, 0 failed |
|
|
| `scripts/test-conductor.sh` | 01:17:54 to 01:18:00 | 17 passed, 0 failed |
|
|
| `scripts/test-config.sh` | 01:18:00 to 01:18:02 | 31 passed, 0 failed |
|
|
| `scripts/test-discord.sh` | 01:18:02 to 01:18:12 | 78 passed, 0 failed |
|
|
| `scripts/test-extension-package.sh` | 01:18:12 to 01:18:13 | 18 passed, 0 failed |
|
|
| `scripts/test-foundation.sh` | 01:18:13 to 01:19:01 | 44 passed, 0 failed |
|
|
| `scripts/test-land-work.sh` | 01:19:01 to 01:19:14 | 86 passed, 0 failed |
|
|
| `scripts/test-queue.sh` | 01:19:14 to 01:19:37 | 156 node tests pass, queue suite 28 passed, 0 failed |
|
|
| `scripts/test-release.sh` | 01:19:37 to 01:19:51 | 14 passed, 0 failed |
|
|
| `scripts/test-task.sh` | 01:19:51 to 01:20:44 | 98 passed, 0 failed |
|
|
|
|
### Mutants, 2026-10-21, same slot
|
|
|
|
`~/darkwing-scratch/r232/mutants.sh`, right after the gate. Both controls (the external file whole, and
|
|
stack-copy's `^row 232: ` tests) exited 0 on the unmutated tree. Each mutant was applied after a checked
|
|
backup and a syntax check, ran its killers from the table above, and was put back with its sha256
|
|
checked. The script stops on any restore mismatch, and it ran to its end.
|
|
|
|
| Mutant | Killer exit | Result |
|
|
|---|---|---|
|
|
| m01 pointer on gate failure | 1 | killed |
|
|
| m02 gate once | 1 | killed |
|
|
| m03 PM from any instance | 1 | killed |
|
|
| m04 no startedAt check | 1 | killed |
|
|
| m05 newest marker | 1 | killed |
|
|
| m06 --to not compared | 1 | killed |
|
|
| m07 one call without --user | 1 | killed |
|
|
| m08 interim pending line | 1 | killed |
|
|
| m09 marker written last | 1 | killed |
|
|
| m10 refusal names the open activation | 1 | killed |
|
|
| m11 stale-start refuses | 1 | killed |
|
|
| m12 revert always restores | 1 | killed |
|
|
| m13 confirm op | 1 | killed |
|
|
| m14 MIGRATING after migrate | 1 | killed |
|
|
| m15 gate floor from the log | 1 | killed |
|
|
| m16 bundle reachability unchecked | 1 | killed |
|
|
| m17 --create overwrites | 1 | killed |
|
|
|
|
17 of 17 killed, 01:22:16 to 01:24:32Z. I read the logs for m01 and m09, two of the quick kills: each failed on
|
|
its named test's assertion (m09 on `AWAITING-UNITS` in sequence 2), not on a load error.
|
|
|
|
## Round 2: rebase onto 8fede5fc6 (sage-1673)
|
|
|
|
Rebase only. Before the landing gate, a check of BASE..HEAD on the manifest paths found that row 230
|
|
(c8bc5cf4c) had changed one of them after the round 1 base, cb5fdb064. Sage ruled a re-round with a
|
|
scoped review, the same shape as row 210 round 3.
|
|
|
|
Branch `darkwing/row232-r2`: the two round 1 commits, a0ffabc3f and 84f1a91e6, cherry-picked onto
|
|
8fede5fc6 with no conflict, then this section. No code or test changed.
|
|
|
|
- `git merge-tree --write-tree origin/refactor darkwing/row232-r1` (refactor at 8fede5fc6) gives tree
|
|
837bde79236c with no conflict. The cherry-picked tip before this section has the same tree.
|
|
- `packages/cli/README.md` is a clean 3-way merge. Row 230's hunk is at lines 710-717 (the session credential
|
|
paragraph); this row's is a new paragraph at 773. With `git patch-id --stable`, the README diff from r1 to
|
|
r2 is 02b679e2, the same as row 230's README diff `cb5fdb064..8fede5fc6`. This row's README diff is
|
|
c395ecd2 on both bases (`cb5fdb064..r1` and `8fede5fc6..r2`).
|
|
- The seven code, test and doc paths other than the README hash the same as in manifest 7a3b4ea1, which
|
|
Filbert approved in round 1: `docs/TOOLS.md`, `packages/cli/tests/stack-copy.test.mjs`,
|
|
`packages/cli/tests/stack-release-external.test.mjs`, `packages/cli/tests/stack-release-fixture.mjs`,
|
|
`packages/cli/tests/stack-release.test.mjs`, `scripts/stack-copy.sh`, `scripts/stack-release.sh`.
|
|
This file also matched until this section was appended. Its other lines are unchanged.
|
|
- The one test that reads the README, `host.test.mjs:855`, looks for the unit's `EnvironmentFile=` line.
|
|
That line is unchanged context in row 230's hunk.
|
|
- No row 230 test reads `stack-release.sh` or `stack-copy.sh`, and no test of this row reads a row 230 file.
|
|
|
|
Nothing was run for this round. The landing gate runs on the r2 tip on Sage's slot.
|
|
|
|
Follow-up, doc-only (sage-1673): `scripts/case2-unit.sh:108` and `scripts/case2-precheck.sh:157` cite
|
|
`scripts/stack-copy.sh:8` and `:22` for `COPIES` and `copy_root`. With this row those lines are `:10` and
|
|
`:26`. Only comments are affected and no test reads them. Row 233 doesn't touch those scripts, so the fix
|
|
goes on the #1732 carry list.
|