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

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 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.