Files
stack/agents/rocko/work/queue-77/r2/BUILD.md
T

227 lines
11 KiB
Markdown

# Row 77 (P19 #225): the Console runs a pinned copy, candidate packet, round 2
Author: Rocko. Reviewers: Darkwing (comment 27801) and Filbert (comment
27818), both "changes" on round 1 candidate `4927de66`. Gate: Sage.
Base: `04db738a` (`base.txt`), current `refactor`, which includes row 61
(`6c42344d`) and row 81 (`c9a5650e`). Row 81 touched
`packages/webui/README.md`; the round 2 change applied to it cleanly with
a 3-way apply. At freeze `refactor` is at `a3f6e9e6`: no commit after
the base touches this row's paths, `packages/tasks` or `packages/bus`,
and `build.patch` passes `git apply --check` on the checkout. The packet
is uncommitted. I made no commits, pushes or
Gitea calls, read no token, and started, stopped or installed no unit.
Tests use `--dir`, a stand-in `systemctl` and fixture copies only.
The round 1 packet is unchanged one level up (`../`). This directory
holds round 2 only; `build.patch` is the whole candidate against the base,
not a delta on round 1.
## Files (`files.txt`, `candidate-manifest.sha256`)
Round 1's eight files, with these round 2 changes:
| File | Round 2 change |
|---|---|
| `scripts/webui-service.sh` | `bus_on_copy`, called by `install`; `packages/tasks/src` and `packages/tasks/fields.json` in `COPY_PATHS`; directories read-only; the temp trap restores write before removing |
| `packages/webui/tests/service.test.mjs` | the stand-in answers the bus `list-units` and `show` calls; the bus-on-copy test |
| `packages/webui/tests/pinned-copy.test.mjs` | directory modes, `sed -i` and a new file refused, the rename-race cleanup test, `GET /api/task-fields` from the copy, a `.refactor` refusal, the README in the copy, `gc.auto 0` in the fixture repository |
| `docs/TOOLS.md`, `packages/webui/README.md` | the bus wording, the install refusal, read-only directories and removal, the rollback floor, the unpinned queue view, the update step not yet usable |
| `agents/rocko/work/queue-77/r2/mutants.py` | 34 mutants (round 1's 18 plus 16) |
| `agents/rocko/work/queue-77/byte-check.sh` | unchanged from round 1 |
Round 1's `agents/rocko/work/queue-77/mutants.py` is superseded by
`r2/mutants.py` and isn't in this candidate.
## Findings
### Darkwing F1, Filbert F1: a Console on a copy fails every human proof
The bus accepts a human proof only when `argv[1]` is its own
`human-cli.mjs` (`human.mjs:70`, `runtime.mjs:68`). A Console on a copy
spawns the copy's `human-cli.mjs`, so with the bus on the checkout every
bus call fails `human-required`.
Fix: `install --copy ID` refuses unless the bus unit runs from the same
copy (`bus_on_copy`, Sage approved the design). It reads the effective
`ExecStart` with `systemctl --user show UNIT -p LoadState -p ExecStart`,
because the live bus has a `pm.conf` drop-in and the unit file alone
isn't the effective value.
- With `--business ID` the unit is `[email protected]`. Without it,
every loaded `mosaic-bus@*.service` from `list-units --all` is checked.
- Every `path=` in `ExecStart` must lie under `<copy>/`. A path that only
shares the copy's prefix (`<copy>x/...`) refuses.
- It refuses when the unit isn't loaded (`not-found`), when no bus unit
is loaded, when `ExecStart` has no path, and when `list-units` or `show`
fails. Each refusal is one line and changes nothing.
- The check runs after `render` and before the running-state read, so
nothing is written on a refusal.
The bus runs from the checkout today, and `copy` doesn't include
`scripts/mosaic` (the bus entry point). So `install --copy` refuses on
the live system until a later row puts the bus on the copy. TOOLS.md and
the README say the update step can't be used yet and why. I didn't add
the bus paths to the copy: moving the bus is the later row (Sage: "Don't
move the bus yourself"), and row 85 (#1579, one-copy stack) is that
row.
TOOLS.md's "the bus child come from the copy" now says the bus child is
the human transport and that the bus checks its path.
Tests (`service.test.mjs`, "install refuses unless the bus unit runs from
the same copy"):
- refused: the bus on the checkout, with and without `--business`; the
prefix-only path; two units, one off the copy; `not-found`; no units
loaded; no `ExecStart` path; `list-units` failing; `show` failing
- accepted: the bus on the same copy, with and without `--business`
The exact-calls assertion in the existing install test now lists the bus
calls too.
### Darkwing F2, Filbert F2: row 61's task field table
`reads.mjs` imports `packages/tasks/src/fields.mjs`, which reads
`packages/tasks/fields.json` at request time. Both are in `COPY_PATHS`.
The comment says why and says the test serves from a copy: a missing
import fails it at load, and a missing file read at request time
(`fields.json`, `RELEASE`, `roles/`) fails the route that reads it, which
the test also GETs (Filbert's wording point).
The serve test GETs `/api/task-fields` from the copy and expects 200 with
a body equal to `packages/tasks/fields.json`. Without that GET, the
"copy misses fields table" mutant survived; with it, it dies.
### Darkwing 3, Filbert N1: read-only directories
`copy` now makes every directory 0555 (the top one included) as well as
every file `a-w`, so a writer that replaces by rename (`sed -i`, git,
editors) fails too. Because a read-only temp directory can't be removed,
the trap runs `chmod -R u+w` before `rm -rf`. TOOLS.md and the README give
the removal command: `chmod -R u+w DIR && rm -rf DIR`.
Tests (`pinned-copy.test.mjs`): every directory is 0555; `sed -i` on a
copied file fails and leaves it unchanged; writing a new file throws
`EACCES`. A new test makes `mv` lose the rename race (a stand-in `mv` that
creates the target and fails): `copy` reports `exists:`, and the copies
directory holds only that one entry, so no read-only temp directory
is left behind.
### Filbert N2: rollback floor (docs)
A copy from before row 77 holds a `cli.mjs` that refuses `--checkout`
("unknown option"), so a unit pointing at it exits 2 at start. I took the
docs-only option Filbert offered: TOOLS.md and the README say rollback
reaches only ids at or after row 77's landing. I didn't add a refusal to
`install`: a grep of the copy's `cli.mjs` would be a second source of
truth for the flag set. Before the next row lands, the update step can't
be used anyway (F1).
### Filbert N3: the queue view isn't pinned (docs)
By design (the brief), the queue read runs from the checkout. The README's
update-step paragraph and TOOLS.md say so: a landing on the queue read
shows at once.
### Filbert's three surviving mutants
| Survivor | Now |
|---|---|
| the commit's leading-character check deleted | killed: `.refactor` refusal case, expects exit 1 and "commit must be a commit id or branch name: .refactor" (mutant "commit may start with a dot") |
| `chmod 0755 "$COPY_TMP"` deleted, top directory 0700 | that line is gone; the directory-mode check covers the top directory (mutants "copy dirs stay writable", "copy subdirs stay writable") |
| `packages/webui/README.md` dropped from `COPY_PATHS` | killed: the copy test checks the README exists and is read-only (mutant "copy misses README") |
## Test flakes seen while building, and one fix
- Round 2's first full run after the mutants had one failure whose name I
didn't capture. To find it I ran the suite five more times with TAP
output. That turned up three failures:
- `bus-browser.test.mjs` "S5 pages in a browser": a 27 s condition
timeout.
- `bus.test.mjs` "humanCall kills a transport that runs past its
timeout": the pid file was missing.
Both are timing failures in files this row doesn't change, seen under a
load average of 20-30.
- The third was this row's own: the cleanup of the new pinned-copy
`tmp` helper (`chmod -R u+w`) failed with ENOENT under the fixture
repository's `.git/objects`. The fixture commits thousands of loose
objects, which starts a detached `git gc --auto`, and gc deletes
objects while chmod walks. I set `gc.auto 0` in the fixture
repository rather than making the cleanup ignore errors.
After that, six consecutive full runs at the earlier base `08941d8b`
were 97 pass, 0 fail (row 81 adds two tests at `04db738a`).
## Mutants (`mutants.py`, `out/mutants.txt`)
`python3 mutants.py <tree>` applies each mutant alone to the candidate
tree and runs `service.test.mjs` and `pinned-copy.test.mjs`. 34 mutants,
all killed. The 16 new ones:
- install skips the bus check
- bus path prefix only
- `not-found` accepted
- `show` failure ignored
- `list-units` failure ignored
- no loaded bus accepted
- no `ExecStart` path accepted
- only the first bus unit checked
- `--business` ignored for the bus
- copy misses tasks src
- copy misses the fields table
- copy misses README
- commit may start with a dot
- copy dirs stay writable, copy subdirs stay writable (`-maxdepth 0`), and
the trap leaves a read-only temp directory
The run in `out/mutants.txt` is on the candidate at `04db738a`.
## Byte check (`out/byte-check.txt`, `../byte-check.sh`)
Round 1's script, base `04db738a` against the candidate: `render`
without `--copy` gives the same unit and exit code for all 8 flag sets,
and the template body is unchanged.
## Gate (`out/gate/`, `out/gate.sh`, `out/after-gate.sh`)
Sequential, in the scratch worktree at `04db738a` with the candidate and
`node_modules` linked, full output kept per suite, load average 17-27:
| Suite | Result |
|---|---|
| webui node (`packages/webui/tests/*.test.mjs`) | 98 pass, 1 fail (below); rerun 99 pass, 0 fail |
| test-auth | 15 passed, 0 failed |
| test-conductor | 17 passed, 0 failed |
| test-config | 24 passed, 0 failed |
| test-discord | 66 passed, 0 failed |
| test-extension-package | 18 passed, 0 failed |
| test-foundation | 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed (the canonical-root checks skip in a worktree, as always) |
| test-release | 14 passed, 0 failed |
| test-task | 98 passed, 0 failed |
The gate's one webui failure: `bus.test.mjs` "humanCall speaks the human
transport protocol without blocking the server" got `outcome-unknown`
where it expects `invalid-response`. That call has a 1500 ms timeout
around spawning a node child, and the load average was 27. Neither
`bus.test.mjs` nor `packages/webui/src/bus.mjs` is in this row's diff.
After the gate, `out/after-gate.sh` ran the whole webui suite again (99
pass, 0 fail) and `bus.test.mjs` alone five times (all pass). The only
failure marker in `out/gate/` is that one, in `node-webui.txt`.
`rocko-r77_default` was removed after the Docker suites.
The TOOLS.md update-step paragraph was reflowed during the gate
(whitespace only, prose); no suite reads `docs/TOOLS.md`. The mutants and
the rerun ran after the reflow.
## For the reviewers
- The live unit and the live bus are untouched. The live Console can't
take this change through `install --copy` until a later row moves the
bus; until then it stays on its current unit.
- The design decision with the most weight: an install refusal rather
than a Console that spawns the checkout's `human-cli.mjs`. The latter
would keep proofs working but put an unpinned file in the pinned
Console's trust path.