11 KiB
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 IDthe unit is[email protected]. Without it, every loadedmosaic-bus@*.servicefromlist-units --allis checked. - Every
path=inExecStartmust 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, whenExecStarthas no path, and whenlist-unitsorshowfails. Each refusal is one line and changes nothing. - The check runs after
renderand 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; noExecStartpath;list-unitsfailing;showfailing - 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
tmphelper (chmod -R u+w) failed with ENOENT under the fixture repository's.git/objects. The fixture commits thousands of loose objects, which starts a detachedgit gc --auto, and gc deletes objects while chmod walks. I setgc.auto 0in the fixture repository rather than making the cleanup ignore errors.
After that, six consecutive full runs at the earlier base
08941d8bwere 97 pass, 0 fail (row 81 adds two tests at04db738a).
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-foundacceptedshowfailure ignoredlist-unitsfailure ignored- no loaded bus accepted
- no
ExecStartpath accepted - only the first bus unit checked
--businessignored 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 --copyuntil 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.