diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 7b8a49c6b..5f868513e 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -4418,3 +4418,9 @@ After (6c42344d): Darkwing's round 1, 12 files +519 -12. `packages/tasks/fields. Before (9438880e): a message Jason sent from the Console got a Sent receipt and nothing else; the reply reached him only through `mosaic talk`, and the broker had no read that returned replies without consuming them. After (c9a5650e): Dewey's round 2, 13 files +455 -30, a rebase on 1e5f5958 after row 61 with round 1's two follow-ups folded in (the cli README's talk entry, a rewrapped `bus.mjs` comment). The broker gains a `replies` read verb for human and reader sessions: `{id}` returns that business's messages whose `in_reply_to` is the id, oldest first, at most 50, writes no `deliveries` row, and refuses `message-not-found` for an unknown id. The Console serves it through `BUS_VERBS` with an id check and the Sent receipt shows the replies; the cli's talk timeout message now says the reply may still come. Approved by Darkwing (27783 round 1, 27821 round 2) on manifest 3ce7527f, no drift. Sage's gate on 9438880e plus the patch, 19:25:18Z to 19:30:39Z: tasks 74, webui 84, bus 102, cli 115, control-board 124, the nine `scripts/test-*.sh` 0 failed (test-task 98). `queue review verify-commit 81 c9a5650e` matched 13 paths. Row 81 done at rev 704 (5849de76), pushed, #1575 closed with comment 27825. The broker verb is live at the next bus restart, which ends the PM session (Mos's pm.conf drop-in relaunches it); the view at the next Console restart, bus first so the view doesn't get unknown-verb. Mos had the decision-109 before-notice; the Console still runs MainPID 2089167 from 19:05:38Z, /healthz 200. + +### 2026-10-12 — Rocko, row 77 (#1571) candidate, round 2: the bus-on-copy refusal, row 61's paths, read-only directories + +Before: round 1 (candidate 4927de66) got changes from Darkwing (27801) and Filbert (27818). A Console on a copy would fail every bus call with `human-required`, because the bus accepts a human proof only from its own `human-cli.mjs` and the bus runs from the checkout. The copy left out row 61's `packages/tasks/src` and `fields.json`, so serve from a copy failed at load, and with `src` alone `/api/task-fields` answered 503. Copy directories stayed writable, so `sed -i` could replace a 0444 file. Filbert left three mutants alive. + +After (candidate, uncommitted, base 04db738a, packet `agents/rocko/work/queue-77/r2/`, candidate manifest sha256 045f0ac8): `install --copy ID` refuses unless every `ExecStart` path of the bus unit (`mosaic-bus@ID` with `--business`, else every loaded `mosaic-bus@` unit), read from `systemctl --user show` so drop-ins count, lies inside that copy. It also refuses when the unit isn't loaded, none is loaded, `ExecStart` has no path, or systemctl fails. `COPY_PATHS` gains `packages/tasks/src` and `packages/tasks/fields.json`, and the serve test GETs `/api/task-fields` from the copy and expects 200. Every copy directory is 0555, and the temp trap runs `chmod -R u+w` before `rm -rf`. TOOLS.md and the README give the removal command, the rollback floor (ids at or after row 77's landing; Filbert N2, docs only), the unpinned queue view (N3), and that the update step can't be used until a later row puts the bus on the copy. Filbert's three survivors are killed by a `.refactor` refusal, the directory-mode check, and the README in the copy. The pinned-copy fixture repository sets `gc.auto 0`: a detached gc was deleting loose objects while the cleanup chmod walked them. Mutants 34/34 killed. Byte check: 8 of 8 renders the same. Gate at 04db738a: the nine `scripts/test-*.sh` 0 failed (test-task 98); webui 98/1 with a `bus.test.mjs` humanCall timeout at load 27 (the file and `bus.mjs` aren't in this diff), rerun 99/0, and `bus.test.mjs` alone 5/5. Moves: in-progress rev 745, in-review round 2 rev 746, request rocko-77-review-2 failed rev 747 (no credential; Sage sends as jarvis). The live unit and the live bus are untouched. diff --git a/agents/rocko/work/queue-77/r2/BUILD.md b/agents/rocko/work/queue-77/r2/BUILD.md new file mode 100644 index 000000000..49fdf2168 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/BUILD.md @@ -0,0 +1,226 @@ +# 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 `mosaic-bus@ID.service`. Without it, + every loaded `mosaic-bus@*.service` from `list-units --all` is checked. +- Every `path=` in `ExecStart` must lie under `/`. A path that only + shares the copy's prefix (`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 ` 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. diff --git a/agents/rocko/work/queue-77/r2/base.txt b/agents/rocko/work/queue-77/r2/base.txt new file mode 100644 index 000000000..4e2b9d0b8 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/base.txt @@ -0,0 +1 @@ +04db738a351e25f6ae92e3c679cffa57887f9368 diff --git a/agents/rocko/work/queue-77/r2/build.patch b/agents/rocko/work/queue-77/r2/build.patch new file mode 100644 index 000000000..e8f6a97ae --- /dev/null +++ b/agents/rocko/work/queue-77/r2/build.patch @@ -0,0 +1,1382 @@ +diff --git a/docs/TOOLS.md b/docs/TOOLS.md +index 89c85715..e52dcf1c 100644 +--- a/docs/TOOLS.md ++++ b/docs/TOOLS.md +@@ -241,8 +241,9 @@ Exit codes: `0` ok · `1` failed or outcome unknown · `2` invalid input · + ## Console service (`scripts/webui-service.sh`) + + ```bash +-scripts/webui-service.sh render [FLAGS] # print the unit +-scripts/webui-service.sh install [--dir DIR] [--no-reload] [FLAGS] ++scripts/webui-service.sh copy COMMIT [--copies DIR] # make a pinned copy ++scripts/webui-service.sh render [--copy ID [--copies DIR]] [FLAGS] # print the unit ++scripts/webui-service.sh install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [FLAGS] + scripts/webui-service.sh uninstall [--dir DIR] [--no-reload] + scripts/webui-service.sh status [--dir DIR] + # FLAGS: [--port N] [--business ID] [--board URL] +@@ -257,19 +258,71 @@ preview set and is refused otherwise (exit 1, nothing written); ExecStart + then ends with `--send`, and the Console needs the system config at start + (`--send needs the bus configured`, exit 2). Without `--send` the unit is + read-only. The template is +-`packages/webui/systemd/mosaic-webui.service.in`. `install` writes a temp +-file, renames it and runs `systemctl --user daemon-reload`; `uninstall` +-refuses while the unit is active; `status` prints the unit state, whether +-the installed unit sends, and `GET /healthz` on the installed port. Each +-value is held to a plain +-character set the unit can carry (no space, `%`, `$`, quote, backslash, +-`&` or `|`) and refused otherwise. The unit sets no `MOSAIC_*`, `CLAUDE*`, +-`CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a port +-already in use) is not restarted. ++`packages/webui/systemd/mosaic-webui.service.in`. ++ ++The unit runs a pinned copy, not the working checkout (row 77, #1571). ++`copy COMMIT` resolves COMMIT to its full id, refuses one not reachable ++from `refs/heads/refactor`, and writes `git archive` of the paths the ++Console loads into `DIR//`, by default ++`${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-console//`. It writes a ++temp directory beside the target, makes every file and directory read-only ++(so a writer that replaces by rename can't change it either), then renames ++it, so the copy's path is always whole. A copy that exists is left alone ++(`exists:`). `install --copy ID` and `render --copy ID` point ++`WorkingDirectory` and ExecStart at the copy and pass serve ++`--checkout `, where the queue read still runs; `RELEASE`, ++the roles, the config check and the bus child come from the copy. The bus ++child is the human transport, `packages/bus/src/human-cli.mjs`, and the bus ++checks that path against its own, so the bus must run from the same copy ++(below). `install` refuses without `--copy`, or when the copy is missing. ++`render` without `--copy` prints the unit for the checkout, as before. ++ ++`install --copy ID` also refuses unless the bus runs from the same copy ++(round 2, #225). The bus accepts a human proof only from its own ++`packages/bus/src/human-cli.mjs`, so a Console on a copy whose bus runs ++from the checkout would fail every bus call with `human-required`. With ++`--business ID`, install reads `mosaic-bus@ID.service`; without it, every ++loaded `mosaic-bus@` unit. It reads the effective ExecStart from ++`systemctl --user show -p LoadState -p ExecStart`, so a drop-in counts, ++and every ExecStart path must lie inside the copy. It refuses, naming both ++paths, when one doesn't; when the unit is not installed or none is loaded; ++when ExecStart has no path; and when the systemctl call fails. The check ++runs before the unit state read and changes nothing. ++ ++`install` writes a temp file, renames it and runs ++`systemctl --user daemon-reload`. `install` and `uninstall` refuse while the ++unit is in any state but inactive or failed (`systemctl --user list-units`), ++and say Jason or Mos stops it first; `--no-reload` skips only the ++daemon-reload, never that check. If the state read itself fails they refuse ++with one line naming the call, and change nothing. `copy` and `install` ++refuse outside the canonical checkout (a `.git` directory, not a worktree's ++`.git` file); `--allow-worktree` overrides that for the tests only. `status` ++prints the unit state, the copy id it runs, whether `refactor` points at ++that commit or another, whether the installed unit sends, and ++`GET /healthz` on the installed port. It changes nothing. ++ ++Each value is held to a plain character set the unit can carry (no space, ++`%`, `$`, quote, backslash, `&` or `|`), and the preview values must start ++with a letter or digit; anything else is refused. With `--auth-header ++X-authentik-uid`, in any letter case, `--allow-user` must be the Authentik ++uid, `^[0-9a-f]{64}$` (lead decision 101). The unit sets no `MOSAIC_*`, ++`CLAUDE*`, `CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a ++port already in use) is not restarted. ++ ++The update step: `copy` the commit, stop the unit, `install --copy ID` ++with the same flags, start it. Rollback is the same steps with the earlier ++id, which must be at or after row 77's landing: an older `cli.mjs` refuses ++serve's `--checkout` ("unknown option"). The queue view isn't pinned: it ++reads the checkout, so it shows the queue as it is now. Copies are kept ++until someone removes them, with `chmod -R u+w DIR && rm -rf DIR` since ++they are read-only; nothing prunes them. The step can't be used yet: the ++bus still runs from the checkout (`copy` doesn't include ++`scripts/mosaic`), so `install --copy ID` refuses until a later row puts ++the bus on the copy. + + There is no start, stop, enable or restart verb. The Console reads the bus + through the human transport, so whoever starts it holds the human's +-capability. Jason starts and restarts it ++capability. Jason starts, stops and restarts it + (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead + decision 102, which accepts that sends from a Console Mos started enter the + trail as Jason (`human-cli`). Any other agent starting it, even through +diff --git a/packages/webui/README.md b/packages/webui/README.md +index be26d050..d4a3628f 100644 +--- a/packages/webui/README.md ++++ b/packages/webui/README.md +@@ -110,6 +110,25 @@ system config at start: without it serve refuses with `--send needs the bus + configured` and exits 2, which the unit doesn't restart. `status` says + whether the installed unit sends. + ++The unit runs a read-only pinned copy of the Console's paths, not the ++working checkout (row 77). `scripts/webui-service.sh copy COMMIT` makes ++one from a commit on `refactor`, and `install --copy ID` points the unit ++at it, with serve's `--checkout` naming the checkout, where the queue read ++still runs, so the queue view isn't pinned and shows the queue as it is ++now. The update step is `copy`, stop, `install --copy ID` with the same ++flags, start; rollback is the same with the earlier id (one at or after ++row 77's landing, since an older `cli.mjs` refuses `--checkout`), and ++copies are kept (they are read-only, files and directories; remove one ++with `chmod -R u+w DIR && rm -rf DIR`). `install --copy ID` refuses unless ++the bus unit runs from the same copy, because the bus accepts a human ++proof only from its own `human-cli.mjs`. The bus still runs from the ++checkout, so the update step can't be used until a later row puts the bus ++on the copy. The stop and start are Jason's or Mos's: `install` and ++`uninstall` refuse while the unit runs. With `--auth-header ++X-authentik-uid` the allowed user must be the 64-character Authentik uid ++(lead decision 101). `status` names the copy and whether `refactor` has ++moved past it. ++ + ## Message a role (row 76, #1570) + + ```sh +diff --git a/packages/webui/src/cli.mjs b/packages/webui/src/cli.mjs +index 618a2cf1..de03a5ee 100644 +--- a/packages/webui/src/cli.mjs ++++ b/packages/webui/src/cli.mjs +@@ -5,9 +5,11 @@ import { views } from '../../bus/src/views.mjs'; + import { loadSystem, socketPath, REPO, rolesDir } from '../../cli/src/config.mjs'; + import { configDir } from '../../business/src/business.mjs'; + import { consoleReads, redactor } from './reads.mjs'; ++import { statSync } from 'node:fs'; ++import { isAbsolute } from 'node:path'; + import { ID_PATTERN } from '../../business/src/vocabulary.mjs'; + const args = process.argv.slice(2); +-const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--board http://127.0.0.1:7331] [--business ID] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]'; ++const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--checkout PATH] [--board http://127.0.0.1:7331] [--business ID] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]'; + try { + if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(usage); process.exit(0); } + if (args.shift() !== 'serve') throw new Error(usage); +@@ -23,6 +25,14 @@ try { + if (!/^\d+$/.test(value) || Number(value) > 65535) throw new Error('port must be 0..65535'); + options.port = Number(value); + } else if (flag === '--board') options.board = value; ++ // Row 77 (#1571): the canonical checkout the queue read runs from, when ++ // serve runs from a pinned copy; everything else loads from the copy. ++ else if (flag === '--checkout') { ++ let dir = false; ++ try { dir = isAbsolute(value) && statSync(value).isDirectory(); } catch { /* refused below */ } ++ if (!dir) throw new Error('--checkout must be the absolute path of a directory'); ++ options.checkout = value; ++ } + else if (flag === '--business') { + if (!ID_PATTERN.test(value)) throw new Error('business must be a business id'); + options.business = value; +@@ -54,10 +64,11 @@ try { + if (sendOn) throw new Error(`--send needs the bus configured (${why})`); + busLine = `Bus: not configured (${why})`; + } +- const reads = consoleReads({ root: REPO, system, configDir: configDir(), rolesDir: rolesDir(), business: options.business ?? null }); ++ const reads = consoleReads({ root: REPO, queueRoot: options.checkout ?? REPO, system, configDir: configDir(), rolesDir: rolesDir(), business: options.business ?? null }); + const server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, send }); + const previewLine = options.preview ? `\nPreview: https://${options.preview.host}/ through the proxy, signed-in user from ${options.preview.header}, ${send ? 'reads and human messages to roles (--send); one line per send below' : 'read-only'}` : ''; +- console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${previewLine}\nCtrl-C stops this server.`); ++ const queueLine = options.checkout ? '\nQueue: read from the --checkout directory' : ''; ++ console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${queueLine}${previewLine}\nCtrl-C stops this server.`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => server.close()); + } catch (err) { + console.error(`refused: ${err.message}`); +diff --git a/packages/webui/src/reads.mjs b/packages/webui/src/reads.mjs +index aa09515e..588db499 100644 +--- a/packages/webui/src/reads.mjs ++++ b/packages/webui/src/reads.mjs +@@ -168,14 +168,16 @@ export function businessView(b, { system = null, now = Date.now() } = {}) { + } + + // --------------------------------------------------------------- all three +-// `system` is loadSystem() output or null. Returns { queue, row, business, ++// `system` is loadSystem() output or null. `queueRoot` is where the queue ++// read runs, `root` otherwise (RELEASE); serve --checkout sets it apart ++// when serve runs from a pinned copy (row 77, #1571). Returns { queue, row, business, + // settings }; each resolves to a redacted body or throws a ReadError whose + // message is redacted. +-export function consoleReads({ root, system = null, configDir = null, rolesDir, business = null, env = process.env, queueTimeoutMs = 30000, now = () => Date.now() }) { ++export function consoleReads({ root, queueRoot = root, system = null, configDir = null, rolesDir, business = null, env = process.env, queueTimeoutMs = 30000, now = () => Date.now() }) { + const dataRoot = system?.dataRoot ?? null; + const redact = redactor({ configDir, dataRoot }); + const fail = (code, message) => new ReadError(code, redact(message)); +- const queue = queueReader({ root, env, timeoutMs: queueTimeoutMs }); ++ const queue = queueReader({ root: queueRoot, env, timeoutMs: queueTimeoutMs }); + const readQueue = async () => { + try { return await queue(); } catch (err) { throw err instanceof ReadError ? fail(err.code, err.message) : fail('read-failed', 'the queue read failed'); } + }; +diff --git a/packages/webui/systemd/mosaic-webui.service.in b/packages/webui/systemd/mosaic-webui.service.in +index 081bd963..6049de0b 100644 +--- a/packages/webui/systemd/mosaic-webui.service.in ++++ b/packages/webui/systemd/mosaic-webui.service.in +@@ -1,13 +1,16 @@ + # Mosaic Console: `node packages/webui/src/cli.mjs serve`, one instance. + # Rendered by scripts/webui-service.sh from packages/webui/systemd/; @REPO@, + # @PATH@, @NODE@ and the serve flags below are filled in at install time. +-# Edit the template and reinstall; do not edit the installed copy. ++# @REPO@ is the read-only copy `install --copy ID` names, and the flags ++# pass the canonical checkout as --checkout for the queue read (row 77). ++# Edit the template and reinstall; do not edit the installed unit. + # + # The Console reads the bus through the human transport, so whoever starts +-# this unit holds the human's capability. Jason starts it from his own +-# shell (`systemctl --user enable --now mosaic-webui`); an agent starting +-# it works around the human proof (lead decisions 62 and 94). The unit sets +-# no MOSAIC_*, CLAUDE*, CODEX* or PI_* variable. ++# this unit holds the human's capability. Jason starts and restarts it ++# (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead ++# decision 102, which accepts that sends then enter the trail as Jason. Any ++# other agent starting it works around the human proof (lead decisions 62 ++# and 94). The unit sets no MOSAIC_*, CLAUDE*, CODEX* or PI_* variable. + [Unit] + Description=Mosaic Console + Documentation=file://@REPO@/packages/webui/README.md +diff --git a/packages/webui/tests/pinned-copy.test.mjs b/packages/webui/tests/pinned-copy.test.mjs +new file mode 100644 +index 00000000..4103f533 +--- /dev/null ++++ b/packages/webui/tests/pinned-copy.test.mjs +@@ -0,0 +1,256 @@ ++import { test } from 'node:test'; ++import assert from 'node:assert/strict'; ++import { execFileSync, spawn } from 'node:child_process'; ++import { once } from 'node:events'; ++import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; ++import { tmpdir } from 'node:os'; ++import { dirname, join } from 'node:path'; ++import { fileURLToPath } from 'node:url'; ++import { fixture } from '../../cli/tests/helpers.mjs'; ++import { consoleReads } from '../src/reads.mjs'; ++ ++// Row 77 (#1571, lead decision 112): `scripts/webui-service.sh copy` and ++// serve from the copy it writes. The copy comes from a fixture repository ++// holding this tree's packages, scripts and roles, so a path the Console ++// loads that the script's list leaves out fails the serve test here. ++// Nothing here reaches systemd: copy makes no systemctl call. ++const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, ''); ++const NODE_DIR = dirname(process.execPath); ++const GIT = ['-c', 'user.name=fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false', '-c', 'core.hooksPath=/dev/null']; ++ ++const git = (cwd, ...args) => execFileSync('git', [...GIT, '-C', cwd, ...args], { encoding: 'utf8' }).trim(); ++ ++// A copy's directories are read-only, so removal gives write back first, ++// as the docs' removal command does. ++function tmp(t, prefix) { ++ const root = mkdtempSync(join(tmpdir(), prefix)); ++ t.after(() => { execFileSync('chmod', ['-R', 'u+w', root]); rmSync(root, { recursive: true, force: true }); }); ++ return root; ++} ++ ++// A canonical checkout (a .git directory) on branch refactor, with this ++// tree's files, the uncommitted ones included, and a RELEASE of its own. ++function fixtureRepo(t) { ++ const dir = tmp(t, 'webui-copy-'); ++ const repo = join(dir, 'repo'); ++ const files = execFileSync('git', ['-C', REPO, 'ls-files', '-z', '-co', '--exclude-standard', '--', 'packages', 'scripts', 'roles', 'RELEASE', ':!packages/*/tests', ':!scripts/foundation'], { encoding: 'utf8' }).split('\0').filter(Boolean); ++ for (const f of files) if (existsSync(join(REPO, f))) cpSync(join(REPO, f), join(repo, f)); ++ writeFileSync(join(repo, 'RELEASE'), '0.0.0-fixture\n'); ++ git(dir, 'init', '-q', '-b', 'refactor', repo); ++ // The commit leaves thousands of loose objects; a detached gc --auto ++ // would still be deleting them when the cleanup's chmod walks .git. ++ git(repo, 'config', 'gc.auto', '0'); ++ git(repo, 'add', '-A'); ++ git(repo, 'commit', '-q', '-m', 'fixture'); ++ return { dir, repo, script: join(repo, 'scripts', 'webui-service.sh'), head: git(repo, 'rev-parse', 'HEAD') }; ++} ++ ++async function run(dir, script, args) { ++ const env = { PATH: `${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') }; ++ const child = spawn(script, args, { env }); ++ let stdout = '', stderr = ''; ++ child.stdout.on('data', (d) => { stdout += d; }); ++ child.stderr.on('data', (d) => { stderr += d; }); ++ const [status] = await once(child, 'close'); ++ return { status, stdout, stderr }; ++} ++ ++function filesUnder(root) { ++ const out = []; ++ for (const entry of readdirSync(root, { withFileTypes: true, recursive: true })) if (entry.isFile()) out.push(join(entry.parentPath, entry.name)); ++ return out; ++} ++ ++test('copy writes the Console paths at the full id, read-only, and leaves an existing copy alone', async (t) => { ++ const { dir, repo, script, head } = fixtureRepo(t); ++ const copies = join(dir, 'copies'); ++ const target = join(copies, head); ++ const r = await run(dir, script, ['copy', head.slice(0, 12), '--copies', copies]); ++ assert.equal(r.status, 0, r.stderr); ++ assert.match(r.stdout, new RegExp(`^copied: ${target}$`, 'm')); ++ assert.deepEqual(readdirSync(copies), [head], 'no temp directory left behind'); ++ assert.deepEqual(readdirSync(target).sort(), ['RELEASE', 'packages', 'roles', 'scripts']); ++ assert.deepEqual(readdirSync(join(target, 'scripts')), ['mosaic-config.mjs']); ++ assert.ok(existsSync(join(target, 'packages', 'webui', 'README.md')), "the unit's Documentation="); ++ assert.equal(statSync(join(target, 'packages', 'webui', 'README.md')).mode & 0o222, 0, 'README.md is read-only'); ++ assert.ok(existsSync(join(target, 'packages', 'tasks', 'fields.json')), 'the task field table'); ++ assert.ok(!existsSync(join(target, 'packages', 'queue')), 'the queue read runs from the checkout'); ++ assert.equal(readFileSync(join(target, 'RELEASE'), 'utf8'), '0.0.0-fixture\n'); ++ const files = filesUnder(target); ++ assert.ok(files.length > 50, String(files.length)); ++ for (const f of files) assert.equal(statSync(f).mode & 0o222, 0, `${f} is writable`); ++ // Directories too, the top one included, so a writer that replaces by ++ // rename can neither change a file nor add one. ++ const dirs = [target, ...readdirSync(target, { withFileTypes: true, recursive: true }).filter((e) => e.isDirectory()).map((e) => join(e.parentPath, e.name))]; ++ assert.ok(dirs.length > 10, String(dirs.length)); ++ for (const d of dirs) assert.equal(statSync(d).mode & 0o777, 0o555, `${d} mode`); ++ const config = join(target, 'scripts', 'mosaic-config.mjs'); ++ const before = readFileSync(config, 'utf8'); ++ const sed = await run(dir, '/usr/bin/sed', ['-i', 's/^/x/', config]); ++ assert.notEqual(sed.status, 0, 'sed -i replaced a file in the copy'); ++ assert.equal(readFileSync(config, 'utf8'), before); ++ assert.throws(() => writeFileSync(join(target, 'scripts', 'added.mjs'), ''), { code: 'EACCES' }); ++ // Already there: left as it is, by id or by branch name. ++ for (const commit of [head, 'refactor']) { ++ const again = await run(dir, script, ['copy', commit, '--copies', copies]); ++ assert.equal(again.status, 0, again.stderr); ++ assert.equal(again.stdout, `exists: ${target}\n`); ++ } ++ // The default directory is under XDG_DATA_HOME, else ~/.local/share. ++ const byDefault = await run(dir, script, ['copy', 'refactor']); ++ assert.equal(byDefault.status, 0, byDefault.stderr); ++ assert.ok(existsSync(join(dir, '.local', 'share', 'mosaic-console', head, 'packages', 'webui', 'src', 'cli.mjs'))); ++ assert.equal(git(repo, 'status', '--porcelain'), '', 'the checkout is untouched'); ++}); ++ ++test('copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory', async (t) => { ++ const { dir, repo, script } = fixtureRepo(t); ++ const copies = join(dir, 'copies'); ++ git(repo, 'checkout', '-q', '-b', 'side'); ++ writeFileSync(join(repo, 'side.txt'), 'side\n'); ++ git(repo, 'add', 'side.txt'); ++ git(repo, 'commit', '-q', '-m', 'side'); ++ const side = git(repo, 'rev-parse', 'HEAD'); ++ git(repo, 'checkout', '-q', 'refactor'); ++ for (const [commit, why] of [[side, new RegExp(`^webui-service: refusing: ${side} is not reachable from refs/heads/refactor$`, 'm')], ['side', /is not reachable from refs\/heads\/refactor/], ['nope', /^webui-service: not a commit: nope$/m], ['a b', /commit must be a commit id or branch name/], ['HEAD~1', /commit must be a commit id or branch name/], ['.refactor', /^webui-service: commit must be a commit id or branch name: \.refactor$/m]]) { ++ const r = await run(dir, script, ['copy', commit, '--copies', copies]); ++ assert.equal(r.status, 1, commit); ++ assert.match(r.stderr, why, commit); ++ assert.equal(r.stdout, '', commit); ++ } ++ for (const args of [['copy'], ['copy', '-x'], ['copy', 'refactor', '--copy', side], ['copy', 'refactor', '--port', '7332'], ['copy', 'refactor', '--dir', dir]]) { ++ assert.equal((await run(dir, script, args)).status, 4, args.join(' ')); ++ } ++ for (const bad of ['copies', `${dir}/a b`, `${dir}/a%b`]) { ++ const r = await run(dir, script, ['copy', 'refactor', '--copies', bad]); ++ assert.equal(r.status, 1, bad); ++ assert.match(r.stderr, /copies directory (must be an absolute path|contains a character the unit cannot carry)/, bad); ++ } ++ assert.ok(!existsSync(copies), 'no refusal writes anything'); ++ // N2: a linked worktree has a .git file; --allow-worktree is for tests. ++ const tree = join(dir, 'tree'); ++ git(repo, 'worktree', 'add', '-q', '--detach', tree, 'refactor'); ++ const fromTree = await run(dir, join(tree, 'scripts', 'webui-service.sh'), ['copy', 'refactor', '--copies', copies]); ++ assert.equal(fromTree.status, 1); ++ assert.match(fromTree.stderr, /^webui-service: refusing: \S+ is not the canonical checkout \(its \.git is not a directory\)/m); ++ assert.ok(!existsSync(copies)); ++ const allowed = await run(dir, join(tree, 'scripts', 'webui-service.sh'), ['copy', 'refactor', '--copies', copies, '--allow-worktree']); ++ assert.equal(allowed.status, 0, allowed.stderr); ++ assert.match(allowed.stdout, /^copied: /m); ++ assert.match((await run(dir, script, ['--help'])).stdout, /--allow-worktree is for the tests only/); ++}); ++ ++test('copy writes nothing when an archived path is missing at the commit', async (t) => { ++ const { dir, repo, script } = fixtureRepo(t); ++ const copies = join(dir, 'copies'); ++ git(repo, 'rm', '-q', 'scripts/mosaic-config.mjs'); ++ git(repo, 'commit', '-q', '-m', 'drop the config check'); ++ const r = await run(dir, script, ['copy', 'refactor', '--copies', copies]); ++ assert.equal(r.status, 1); ++ assert.match(r.stderr, /^webui-service: git archive of [0-9a-f]{40} failed; no copy written$/m); ++ assert.deepEqual(readdirSync(copies), [], 'the temp directory is removed'); ++}); ++ ++test('a copy that loses the rename race leaves no read-only temp directory behind', async (t) => { ++ const { dir, script, head } = fixtureRepo(t); ++ const copies = join(dir, 'copies'); ++ // A stand-in mv that makes the target appear first, as a concurrent copy ++ // would, then fails: copy says exists: and its trap removes the temp copy. ++ const bin = join(dir, 'bin'); ++ mkdirSync(bin); ++ writeFileSync(join(bin, 'mv'), `#!/bin/sh\nfor a; do t="$a"; done\nmkdir "$t"\nexit 1\n`, { mode: 0o755 }); ++ const env = { PATH: `${bin}:${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') }; ++ const child = spawn(script, ['copy', 'refactor', '--copies', copies], { env }); ++ let stdout = ''; ++ child.stdout.on('data', (d) => { stdout += d; }); ++ const [status] = await once(child, 'close'); ++ assert.equal(status, 0); ++ assert.equal(stdout, `exists: ${join(copies, head)}\n`); ++ assert.deepEqual(readdirSync(copies), [head], 'the temp directory is removed'); ++}); ++ ++// serve from a copy: /healthz, the config check, roles and RELEASE from the ++// copy, and the queue read from --checkout or, without it, from the copy. ++async function serve(t, cli, args, env) { ++ const child = spawn(process.execPath, [cli, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', ...args], { env, stdio: ['ignore', 'pipe', 'pipe'] }); ++ t.after(() => child.kill()); ++ let out = '', err = ''; ++ child.stderr.on('data', (c) => { err += c; }); ++ const port = await new Promise((resolve, reject) => { ++ child.stdout.on('data', (c) => { out += c; const m = out.match(/^Mosaic Console: http:\/\/127\.0\.0\.1:(\d+)\/$/m); if (m && out.includes('Ctrl-C')) resolve(Number(m[1])); }); ++ child.on('exit', (code) => reject(new Error(`exited ${code}: ${out}${err}`))); ++ }); ++ const get = async (path) => { const res = await fetch(`http://127.0.0.1:${port}${path}`); return { status: res.status, body: await res.json() }; }; ++ return { out, get }; ++} ++ ++test('serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout', async (t) => { ++ const { dir, script, head } = fixtureRepo(t); ++ const copies = join(dir, 'copies'); ++ assert.equal((await run(dir, script, ['copy', 'refactor', '--copies', copies])).status, 0); ++ const cli = join(copies, head, 'packages', 'webui', 'src', 'cli.mjs'); ++ // A checkout whose queue read says where it ran. ++ const checkout = join(dir, 'checkout'); ++ mkdirSync(join(checkout, 'packages', 'webui', 'src'), { recursive: true }); ++ writeFileSync(join(checkout, 'packages', 'webui', 'src', 'queue-read.mjs'), `process.stdout.write(JSON.stringify({ rows: [], notes: [process.cwd() === ${JSON.stringify(checkout)} ? 'read in the checkout' : 'read elsewhere'] }));\n`); ++ const { env } = fixture(join(dir, 'home')); ++ delete env.MOSAIC_ROLES_DIR; ++ const { out, get } = await serve(t, cli, ['--business', 'acme', '--checkout', checkout], env); ++ assert.match(out, /^Bus: acme$/m, 'the config check ran from the copy'); ++ assert.match(out, /^Queue: read from the --checkout directory$/m); ++ assert.deepEqual(await get('/healthz'), { status: 200, body: { ok: true } }); ++ const settings = await get('/api/settings'); ++ assert.equal(settings.status, 200); ++ assert.equal(settings.body.release, '0.0.0-fixture', 'RELEASE from the copy'); ++ const business = await get('/api/business'); ++ assert.equal(business.status, 200, JSON.stringify(business.body)); ++ assert.equal(business.body.business.id, 'acme', 'roles from the copy'); ++ const queue = await get('/api/queue'); ++ assert.equal(queue.status, 200, JSON.stringify(queue.body)); ++ assert.deepEqual(queue.body.notes, ['read in the checkout']); ++ // Row 61's field table loads from the copy's packages/tasks. ++ const fields = await get('/api/task-fields'); ++ assert.equal(fields.status, 200, JSON.stringify(fields.body)); ++ assert.deepEqual(fields.body, JSON.parse(readFileSync(join(REPO, 'packages', 'tasks', 'fields.json'), 'utf8'))); ++ // Without --checkout the copy's own queue read runs, and the copy holds ++ // no packages/queue, so it fails closed. ++ const plain = await serve(t, cli, ['--business', 'acme'], env); ++ assert.doesNotMatch(plain.out, /^Queue:/m); ++ assert.equal((await plain.get('/healthz')).status, 200); ++ const none = await plain.get('/api/queue'); ++ assert.equal(none.status, 503); ++ assert.equal(none.body.error, 'read-failed'); ++}); ++ ++test('serve refuses a --checkout that is not an absolute directory path', async (t) => { ++ const dir = tmp(t, 'webui-checkout-'); ++ writeFileSync(join(dir, 'file'), ''); ++ // 'sub' is a directory under the child's cwd, so only its being relative refuses it. ++ mkdirSync(join(dir, 'sub')); ++ for (const value of ['sub', join(dir, 'missing'), join(dir, 'file')]) { ++ const child = spawn(process.execPath, [join(REPO, 'packages', 'webui', 'src', 'cli.mjs'), 'serve', '--port', '0', '--checkout', value], { cwd: dir, timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] }); ++ let out = '', err = ''; ++ child.stdout.on('data', (c) => { out += c; }); ++ child.stderr.on('data', (c) => { err += c; }); ++ const [code] = await once(child, 'close'); ++ assert.equal(code, 2, value); ++ assert.equal(err, 'refused: --checkout must be the absolute path of a directory\n', value); ++ assert.equal(out, ''); ++ } ++}); ++ ++test('consoleReads runs the queue read under queueRoot and reads RELEASE under root', async (t) => { ++ const dir = tmp(t, 'webui-roots-'); ++ const [root, queueRoot] = [join(dir, 'copy'), join(dir, 'checkout')]; ++ for (const [base, said] of [[root, 'copy'], [queueRoot, 'checkout']]) { ++ mkdirSync(join(base, 'packages', 'webui', 'src'), { recursive: true }); ++ writeFileSync(join(base, 'packages', 'webui', 'src', 'queue-read.mjs'), `process.stdout.write(JSON.stringify({ rows: [], notes: ['${said}', process.cwd() === ${JSON.stringify(base)} ? 'cwd there' : 'cwd elsewhere'] }));\n`); ++ writeFileSync(join(base, 'RELEASE'), `${said}-release\n`); ++ } ++ const apart = consoleReads({ root, queueRoot, rolesDir: join(dir, 'roles') }); ++ assert.deepEqual((await apart.queue()).notes, ['checkout', 'cwd there'], 'the checkout ran it, with its cwd there'); ++ assert.equal((await apart.settings()).release, 'copy-release'); ++ const together = consoleReads({ root, rolesDir: join(dir, 'roles') }); ++ assert.deepEqual((await together.queue()).notes, ['copy', 'cwd there'], 'queueRoot defaults to root'); ++ assert.equal((await together.settings()).release, 'copy-release'); ++}); +diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs +index da440d6b..d1226b19 100644 +--- a/packages/webui/tests/service.test.mjs ++++ b/packages/webui/tests/service.test.mjs +@@ -11,22 +11,35 @@ import { fileURLToPath } from 'node:url'; + // scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or + // reaches the real systemd: install and uninstall run with --no-reload or a + // stand-in systemctl on PATH, and XDG_CONFIG_HOME points into a temp dir. +-// Row 83 (#1576) adds --send, taken only with the preview set. ++// Row 83 (#1576) adds --send, taken only with the preview set. Row 77 ++// (#1571) adds --copy, the running-unit refusals and the uid rule; every run ++// gets a stand-in systemctl, since install now reads the unit's state, and ++// install runs with --allow-worktree, since the suite runs in worktrees. + const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, ''); + const SCRIPT = join(REPO, 'scripts', 'webui-service.sh'); + const NODE_DIR = dirname(process.execPath); +-const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason']; +-const PREVIEW_FLAGS = '--public-host console.example.com --auth-header X-authentik-uid --allow-user jason'; ++const UID = '6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e'; ++const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', UID]; ++const PREVIEW_FLAGS = `--public-host console.example.com --auth-header X-authentik-uid --allow-user ${UID}`; ++// A copy as `copy` leaves it, reduced to the file install looks for. ++const COPY_ID = '0123456789abcdef0123456789abcdef01234567'; ++const INSTALL = ['--copy', COPY_ID, '--allow-worktree']; ++const copyRoot = (dir) => join(dir, '.local', 'share', 'mosaic-console', COPY_ID); ++function fakeCopy(dir) { ++ mkdirSync(join(copyRoot(dir), 'packages', 'webui', 'src'), { recursive: true }); ++ writeFileSync(join(copyRoot(dir), 'packages', 'webui', 'src', 'cli.mjs'), ''); ++ return copyRoot(dir); ++} + const SEND_REFUSAL = /^webui-service: --send needs the preview mode \(--public-host, --auth-header and --allow-user\)$/m; + const TEMPLATE = readFileSync(join(REPO, 'packages', 'webui', 'systemd', 'mosaic-webui.service.in'), 'utf8'); + + // The unit as the base script renders it (row 75): the template with its four + // placeholders filled in, nothing else. Row 83 adds --send and must leave + // every unit rendered without it byte for byte as this. +-function filled(flags) { ++function filled(flags, root = REPO) { + const base = '/usr/local/bin:/usr/bin:/bin'; + const path = `:${base}:`.includes(`:${NODE_DIR}:`) ? base : `${NODE_DIR}:${base}`; +- return TEMPLATE.replaceAll('@REPO@', REPO).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags); ++ return TEMPLATE.replaceAll('@REPO@', root).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags); + } + + // A loopback port with nothing listening, so status's /healthz request +@@ -44,18 +57,35 @@ async function closedPort() { + function tmp(t) { + const dir = mkdtempSync(join(tmpdir(), 'webui-service-')); + t.after(() => rmSync(dir, { recursive: true, force: true })); ++ fakeCopy(dir); + return dir; + } + +-// A stand-in systemctl that prints `lines` for list-units and logs every call. +-function standIn(dir, lines = '') { ++// The bus unit as `systemctl show -p LoadState -p ExecStart` prints it, ++// started from `root`. ++const busShow = (root) => `LoadState=loaded\nExecStart={ path=${root}/scripts/mosaic ; argv[]=${root}/scripts/mosaic bus start acme ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\n`; ++const BUS_UNITS = 'mosaic-bus@acme.service loaded active running Mosaic bus host (acme)\n'; ++ ++// A stand-in systemctl that prints `lines` for the Console's list-units and ++// logs every call. With `listFails`, that list-units exits 1 after printing ++// to stderr. The bus calls install makes are answered apart: `busUnits` for ++// the mosaic-bus@ list-units, and `bus` for show, by default a bus started ++// from the run's own copy ($HOME, as run() sets it). `busFails` makes both ++// bus calls fail. ++function standIn(dir, lines = '', { listFails = false, bus = null, busUnits = BUS_UNITS, busFails = false } = {}) { + const bin = join(dir, 'bin'); + mkdirSync(bin); + writeFileSync(join(dir, 'list-units.txt'), lines); ++ writeFileSync(join(dir, 'bus-units.txt'), busUnits); ++ if (bus !== null) writeFileSync(join(dir, 'bus-show.txt'), bus); ++ const fail = 'echo "Failed to connect to bus" >&2; exit 1'; ++ const show = bus !== null ? `cat "${dir}/bus-show.txt"` : `r="$HOME/.local/share/mosaic-console/${COPY_ID}"; printf '%s' "${busShow('$r')}"`; + writeFileSync(join(bin, 'systemctl'), `#!/bin/sh + echo "$*" >> "${dir}/calls.txt" + case "$*" in +- *list-units*) cat "${dir}/list-units.txt" ;; ++ *list-units*mosaic-bus@*) ${busFails ? fail : `cat "${dir}/bus-units.txt"`} ;; ++ *show*mosaic-bus@*) ${busFails ? fail : show} ;; ++ *list-units*) ${listFails ? fail : `cat "${dir}/list-units.txt"`} ;; + *is-enabled*) echo enabled ;; + *is-active*) echo active ;; + esac +@@ -65,9 +95,20 @@ exit 0 + return bin; + } + ++// Without a stand-in of its own, a run gets one that lists nothing (the ++// unit not loaded), so no run reaches the real systemd. ++function quiet(dir) { ++ const home = join(dir, '.quiet'); ++ if (!existsSync(home)) { mkdirSync(home); standIn(home); } ++ return join(home, 'bin'); ++} ++// Every call but the reads: the Console's state and install's bus check. ++const READS = /^--user (list-units --no-legend --plain mosaic-webui\.service|list-units --all --no-legend --plain mosaic-bus@\*\.service|show mosaic-bus@\S+ -p LoadState -p ExecStart)$/; ++const otherCalls = (file) => readFileSync(file, 'utf8').split('\n').filter(Boolean).filter((c) => !READS.test(c)); ++ + // Async, so an in-process server can answer the script's /healthz request. + async function run(dir, args, bin = null) { +- const env = { PATH: `${bin ? `${bin}:` : ''}${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') }; ++ const env = { PATH: `${bin ?? quiet(dir)}:${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') }; + const child = spawn(SCRIPT, args, { env }); + let stdout = '', stderr = ''; + child.stdout.on('data', (d) => { stdout += d; }); +@@ -95,7 +136,7 @@ test('render carries every serve flag, the preview set included', async (t) => { + const dir = tmp(t); + const r = await run(dir, ['render', '--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW]); + assert.equal(r.status, 0, r.stderr); +- assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 --public-host console.example.com --auth-header X-authentik-uid --allow-user jason`); ++ assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 --public-host console.example.com --auth-header X-authentik-uid --allow-user ${UID}`); + }); + + test('a partial preview set is refused', async (t) => { +@@ -153,8 +194,9 @@ test('install into --dir with --no-reload writes the file whole, then reports un + const dir = tmp(t); + const unitDir = join(dir, 'units'); + const bin = standIn(dir); +- const rendered = await run(dir, ['render', '--business', 'mosaic']); +- const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin); ++ const rendered = await run(dir, ['render', '--copy', COPY_ID, '--business', 'mosaic']); ++ assert.equal(rendered.stdout, filled(`--checkout ${REPO} --port 7332 --business mosaic`, copyRoot(dir))); ++ const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin); + assert.equal(r.status, 0, r.stderr); + const target = join(unitDir, 'mosaic-webui.service'); + assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm')); +@@ -162,41 +204,50 @@ test('install into --dir with --no-reload writes the file whole, then reports un + assert.equal(readFileSync(target, 'utf8'), rendered.stdout); + assert.equal(statSync(target).mode & 0o777, 0o644); + assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service'], 'no temp file left behind'); +- const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin); ++ const again = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin); + assert.equal(again.status, 0, again.stderr); + assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm')); +- assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call with --no-reload'); ++ assert.deepEqual(otherCalls(join(dir, 'calls.txt')), [], 'with --no-reload, only the state read'); + // A refused value writes nothing and leaves the installed unit alone. +- const bad = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'a b'], bin); ++ const bad = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--business', 'a b'], bin); + assert.equal(bad.status, 1); + assert.equal(readFileSync(target, 'utf8'), rendered.stdout); + assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']); + const fresh = join(dir, 'never'); +- assert.equal((await run(dir, ['install', '--dir', fresh, '--no-reload', '--port', '0'], bin)).status, 1); ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', fresh, '--no-reload', '--port', '0'], bin)).status, 1); + assert.ok(!existsSync(fresh), 'a refusal creates no directory'); + }); + + test('install without --no-reload runs daemon-reload and nothing that starts the unit', async (t) => { + const dir = tmp(t); + const bin = standIn(dir); +- const r = await run(dir, ['install', '--dir', join(dir, 'units')], bin); ++ const r = await run(dir, ['install', ...INSTALL, '--dir', join(dir, 'units')], bin); + assert.equal(r.status, 0, r.stderr); + assert.match(r.stdout, /^daemon-reload done$/m); +- assert.equal(readFileSync(join(dir, 'calls.txt'), 'utf8'), '--user daemon-reload\n'); ++ assert.equal(readFileSync(join(dir, 'calls.txt'), 'utf8'), '--user list-units --all --no-legend --plain mosaic-bus@*.service\n--user show mosaic-bus@acme.service -p LoadState -p ExecStart\n--user list-units --no-legend --plain mosaic-webui.service\n--user daemon-reload\n'); + }); + +-test('uninstall refuses while the unit is active, and removes it once it is not', async (t) => { ++test('uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not', async (t) => { + const dir = tmp(t); + const unitDir = join(dir, 'units'); + const target = join(unitDir, 'mosaic-webui.service'); +- for (const state of ['active', 'activating', 'deactivating']) { ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0); ++ for (const state of ['active', 'activating', 'deactivating', 'reloading']) { + const bin = standIn(mkdtempSync(join(dir, 'si-')), `mosaic-webui.service loaded ${state} running Mosaic Console\n`); +- assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload'])).status, 0); +- const r = await run(dir, ['uninstall', '--dir', unitDir], bin); +- assert.equal(r.status, 1, state); +- assert.match(r.stderr, /refusing: mosaic-webui\.service is still running/); +- assert.ok(existsSync(target), `still installed while ${state}`); ++ // N3: --no-reload skips only daemon-reload, never the state read. ++ for (const args of [['uninstall', '--dir', unitDir], ['uninstall', '--dir', unitDir, '--no-reload']]) { ++ const r = await run(dir, args, bin); ++ assert.equal(r.status, 1, `${state} ${args.join(' ')}`); ++ assert.equal(r.stderr, `webui-service: refusing: mosaic-webui.service is still running (${state}); Jason or Mos stops it first (systemctl --user stop mosaic-webui)\n`); ++ assert.equal(r.stdout, ''); ++ assert.ok(existsSync(target), `still installed while ${state}`); ++ } + } ++ // A failed unit is not running; uninstall goes ahead. ++ const failed = standIn(mkdtempSync(join(dir, 'si-')), 'mosaic-webui.service loaded failed failed Mosaic Console\n'); ++ assert.equal((await run(dir, ['uninstall', '--dir', unitDir, '--no-reload'], failed)).status, 0); ++ assert.ok(!existsSync(target)); ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0); + const bin = standIn(mkdtempSync(join(dir, 'si-')), 'mosaic-webui.service loaded inactive dead Mosaic Console\n'); + const r = await run(dir, ['uninstall', '--dir', unitDir], bin); + assert.equal(r.status, 0, r.stderr); +@@ -220,9 +271,10 @@ test('status prints the unit state and GET /healthz on the installed port', asyn + await once(server, 'listening'); + t.after(() => server.close()); + const { port } = server.address(); +- assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port)])).status, 0); ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port)])).status, 0); + const r = await run(dir, ['status', '--dir', unitDir], bin); + assert.equal(r.status, 0, r.stderr); ++ assert.match(r.stdout, new RegExp(`^copy: ${COPY_ID}$`, 'm')); + assert.match(r.stdout, /^ {2}enabled: enabled$/m); + assert.match(r.stdout, /^ {2}active: {2}active$/m); + assert.match(r.stdout, new RegExp(`^healthz: http://127\\.0\\.0\\.1:${port}/healthz 200 \\{"ok":true\\}$`, 'm')); +@@ -237,7 +289,7 @@ test('status prints the unit state and GET /healthz on the installed port', asyn + + test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => { + const dir = tmp(t); +- for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload'], ['render', '--send', 'yes', ...PREVIEW]]) { ++ for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload'], ['render', '--send', 'yes', ...PREVIEW], ['render', '--allow-worktree'], ['render', '--copies', dir], ['uninstall', '--copy', COPY_ID], ['uninstall', '--allow-worktree'], ['status', '--copy', COPY_ID], ['status', '--allow-worktree'], ['install', '--copy']]) { + const r = await run(dir, args); + assert.equal(r.status, 4, args.join(' ')); + assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/); +@@ -268,7 +320,7 @@ test('render --send with the preview set ends ExecStart with --send and changes + const r = await run(dir, ['render', ...args]); + assert.equal(r.status, 0, r.stderr); + assert.equal(r.stdout, filled(`${flags} --send`), args.join(' ')); +- assert.ok(execStart(r.stdout).endsWith(' --allow-user jason --send'), execStart(r.stdout)); ++ assert.ok(execStart(r.stdout).endsWith(` --allow-user ${UID} --send`), execStart(r.stdout)); + const without = await run(dir, ['render', ...plain]); + assert.equal(without.status, 0, without.stderr); + const a = r.stdout.split('\n'), b = without.stdout.split('\n'); +@@ -290,20 +342,20 @@ test('--send without the whole preview set is refused and writes nothing', async + const bin = standIn(dir); + const fresh = join(dir, 'never'); + for (const args of partial) { +- const r = await run(dir, ['install', '--dir', fresh, '--no-reload', ...args, '--send'], bin); ++ const r = await run(dir, ['install', ...INSTALL, '--dir', fresh, '--no-reload', ...args, '--send'], bin); + assert.equal(r.status, 1, args.join(' ')); + assert.match(r.stderr, SEND_REFUSAL, args.join(' ')); + } + assert.ok(!existsSync(fresh), 'a refusal creates no directory'); + const unitDir = join(dir, 'units'); + const target = join(unitDir, 'mosaic-webui.service'); +- assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW], bin)).status, 0); ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', ...PREVIEW], bin)).status, 0); + const before = readFileSync(target, 'utf8'); +- const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--send', ...PREVIEW.slice(0, 4)], bin); ++ const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--send', ...PREVIEW.slice(0, 4)], bin); + assert.equal(r.status, 1); + assert.equal(readFileSync(target, 'utf8'), before, 'the installed unit is left alone'); + assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']); +- assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call'); ++ assert.deepEqual(otherCalls(join(dir, 'calls.txt')), [], 'no systemctl call but the one state read'); + }); + + test('install --send into --dir with --no-reload writes the sending unit, and status says whether it sends', async (t) => { +@@ -312,13 +364,14 @@ test('install --send into --dir with --no-reload writes the sending unit, and st + const target = join(unitDir, 'mosaic-webui.service'); + const port = await closedPort(); + const flags = `--port ${port} ${PREVIEW_FLAGS}`; +- const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send']); ++ const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send']); + assert.equal(r.status, 0, r.stderr); + assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm')); + // Lead decision 102: Jason or Mos starts it, no other agent. + assert.match(r.stdout, /^this script starts nothing: .* Jason\nstarts and restarts it, or Mos under lead decision 102 .*\ntrail as Jason\); no other agent does:$/m); +- assert.match(r.stdout, new RegExp(`^serve: \\S+ ${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/packages/webui/src/cli\\.mjs serve ${flags} --send$`, 'm')); +- assert.equal(readFileSync(target, 'utf8'), filled(`${flags} --send`)); ++ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); ++ assert.match(r.stdout, new RegExp(`^serve: \\S+ ${esc(copyRoot(dir))}/packages/webui/src/cli\\.mjs serve --checkout ${esc(REPO)} ${flags} --send$`, 'm')); ++ assert.equal(readFileSync(target, 'utf8'), filled(`--checkout ${REPO} ${flags} --send`, copyRoot(dir))); + assert.equal(statSync(target).mode & 0o777, 0o644); + assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']); + const bin = standIn(dir); +@@ -326,34 +379,34 @@ test('install --send into --dir with --no-reload writes the sending unit, and st + assert.equal(sending.status, 0, sending.stderr); + assert.match(sending.stdout, /^send: on \(--send; human messages to roles\)$/m); + assert.match(sending.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m); +- const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), '--send', ...PREVIEW], bin); ++ const again = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--send', ...PREVIEW], bin); + assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm')); +- const readOnly = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW], bin); ++ const readOnly = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW], bin); + assert.match(readOnly.stdout, new RegExp(`^written: ${target}$`, 'm')); +- assert.equal(readFileSync(target, 'utf8'), filled(flags)); ++ assert.equal(readFileSync(target, 'utf8'), filled(`--checkout ${REPO} ${flags}`, copyRoot(dir))); + const off = await run(dir, ['status', '--dir', unitDir], bin); + assert.match(off.stdout, /^send: off \(read-only\)$/m); + assert.doesNotMatch(off.stdout, /^send: on/m); +- // The only systemctl calls are status's reads; install ran none. +- assert.deepEqual(readFileSync(join(dir, 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []); ++ // The only systemctl calls are status's reads and install's state read. ++ assert.deepEqual(otherCalls(join(dir, 'calls.txt')).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []); + const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin); + assert.doesNotMatch(none.stdout, /^send:/m, 'no unit, no send line'); +- // serve takes --send as an allowed user, so ExecStart can end with +- // `--allow-user --send` on a unit that doesn't send. +- const oddUser = [...PREVIEW.slice(0, 4), '--allow-user', '--send']; +- for (const [args, ending, state] of [[oddUser, '--allow-user --send', 'off'], [[...oddUser, '--send'], '--allow-user --send --send', 'on']]) { +- const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...args]); +- assert.equal(r.status, 0, r.stderr); +- assert.ok(execStart(readFileSync(target, 'utf8')).endsWith(` ${ending}`), args.join(' ')); +- assert.match((await run(dir, ['status', '--dir', unitDir], bin)).stdout, new RegExp(`^send: ${state} `, 'm'), args.join(' ')); ++ // serve would take --send as an allowed user; N5 refuses it, so no unit ++ // ends with `--allow-user --send` without sending. ++ const before = readFileSync(target, 'utf8'); ++ for (const args of [['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--send'], ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--send', '--send']]) { ++ const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...args]); ++ assert.equal(r.status, 1, args.join(' ')); ++ assert.match(r.stderr, /^webui-service: allowed user must start with a letter or digit: --send$/m); + } ++ assert.equal(readFileSync(target, 'utf8'), before); + }); + + test('uninstall and status take no --send', async (t) => { + const dir = tmp(t); + const bin = standIn(dir); + const unitDir = join(dir, 'units'); +- assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW, '--send'])).status, 0); ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', ...PREVIEW, '--send'])).status, 0); + for (const args of [['uninstall', '--dir', unitDir, '--send'], ['uninstall', '--dir', unitDir, '--no-reload', '--send'], ['status', '--dir', unitDir, '--send']]) { + const r = await run(dir, args, bin); + assert.equal(r.status, 4, args.join(' ')); +@@ -363,3 +416,234 @@ test('uninstall and status take no --send', async (t) => { + assert.ok(existsSync(join(unitDir, 'mosaic-webui.service')), 'still installed'); + assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call'); + }); ++ ++// Row 77 (#1571) from here on. ++const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); ++ ++test('install refuses while the unit is running, in any state but inactive or failed, and changes nothing', async (t) => { ++ const dir = tmp(t); ++ const unitDir = join(dir, 'units'); ++ const target = join(unitDir, 'mosaic-webui.service'); ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0); ++ const before = readFileSync(target, 'utf8'); ++ for (const state of ['active', 'activating', 'deactivating', 'reloading']) { ++ const home = mkdtempSync(join(dir, 'si-')); ++ const bin = standIn(home, `mosaic-webui.service loaded ${state} running Mosaic Console\n`); ++ for (const reload of [[], ['--no-reload']]) { ++ const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, ...reload, '--port', '7444'], bin); ++ assert.equal(r.status, 1, `${state} ${reload}`); ++ assert.equal(r.stderr, `webui-service: refusing: mosaic-webui.service is running (${state}); Jason or Mos stops it first (systemctl --user stop mosaic-webui), then install, then start it\n`); ++ assert.equal(r.stdout, ''); ++ } ++ assert.deepEqual(otherCalls(join(home, 'calls.txt')), [], 'no daemon-reload'); ++ assert.equal(readFileSync(target, 'utf8'), before, `unit left alone while ${state}`); ++ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service'], 'no temp file left'); ++ } ++ // inactive, failed, another unit's line, or nothing listed: install goes ahead. ++ for (const lines of ['mosaic-webui.service loaded inactive dead Mosaic Console\n', 'mosaic-webui.service loaded failed failed Mosaic Console\n', 'mosaic-bus@x.service loaded active running Bus\n', '']) { ++ const bin = standIn(mkdtempSync(join(dir, 'si-')), lines); ++ const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', '7444'], bin); ++ assert.equal(r.status, 0, `${JSON.stringify(lines)}: ${r.stderr}`); ++ } ++}); ++ ++test('when the state read fails, install and uninstall refuse with one line and change nothing', async (t) => { ++ const dir = tmp(t); ++ const unitDir = join(dir, 'units'); ++ const target = join(unitDir, 'mosaic-webui.service'); ++ assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0); ++ const before = readFileSync(target, 'utf8'); ++ const home = mkdtempSync(join(dir, 'si-')); ++ const bin = standIn(home, '', { listFails: true }); ++ const refusal = 'webui-service: refusing: systemctl --user list-units --no-legend --plain mosaic-webui.service failed, so the unit\'s state is unknown; nothing was changed\n'; ++ for (const args of [['install', ...INSTALL, '--dir', unitDir, '--port', '7444'], ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', '7444'], ['uninstall', '--dir', unitDir], ['uninstall', '--dir', unitDir, '--no-reload']]) { ++ const r = await run(dir, args, bin); ++ assert.equal(r.status, 1, args.join(' ')); ++ assert.equal(r.stderr, refusal, args.join(' ')); ++ assert.equal(r.stdout, ''); ++ } ++ assert.deepEqual(otherCalls(join(home, 'calls.txt')), [], 'no daemon-reload'); ++ assert.equal(readFileSync(target, 'utf8'), before); ++ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']); ++}); ++ ++test('install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree', async (t) => { ++ const dir = tmp(t); ++ const unitDir = join(dir, 'units'); ++ const cases = [ ++ [[], /^webui-service: install needs --copy ID, a copy made with: scripts\/webui-service\.sh copy COMMIT$/m], ++ [['--copy', COPY_ID.slice(0, 12)], /^webui-service: --copy must be a full 40-character commit id: 0123456789ab$/m], ++ [['--copy', COPY_ID.toUpperCase()], /^webui-service: --copy must be a full 40-character commit id: /m], ++ [['--copy', `${COPY_ID}0`], /^webui-service: --copy must be a full 40-character commit id: /m], ++ [['--copy', 'f'.repeat(40)], new RegExp(`^webui-service: no copy ${'f'.repeat(40)} in ${esc(join(dir, '.local', 'share', 'mosaic-console'))}; make it with: scripts/webui-service\\.sh copy ${'f'.repeat(40)}$`, 'm')], ++ [['--copy', COPY_ID, '--copies', 'relative'], /^webui-service: copies directory must be an absolute path: relative$/m], ++ [['--copy', COPY_ID, '--copies', join(dir, 'no copies')], /^webui-service: copies directory contains a character the unit cannot carry: /m], ++ ]; ++ for (const [copy, refusal] of cases) { ++ const r = await run(dir, ['install', '--allow-worktree', ...copy, '--dir', unitDir, '--no-reload']); ++ assert.equal(r.status, 1, copy.join(' ')); ++ assert.match(r.stderr, refusal, copy.join(' ')); ++ assert.ok(!existsSync(unitDir), copy.join(' ')); ++ } ++ // This suite runs in a git worktree, whose .git is a file: without ++ // --allow-worktree, install refuses before it reads anything else. ++ if (!statSync(join(REPO, '.git')).isDirectory()) { ++ const r = await run(dir, ['install', '--copy', COPY_ID, '--dir', unitDir, '--no-reload']); ++ assert.equal(r.status, 1); ++ assert.equal(r.stderr, `webui-service: refusing: ${REPO} is not the canonical checkout (its .git is not a directory); run copy and install from the canonical checkout\n`); ++ assert.ok(!existsSync(unitDir)); ++ } ++ // --copies moves where the copy is looked for. ++ const other = join(dir, 'other'); ++ mkdirSync(join(other, COPY_ID, 'packages', 'webui', 'src'), { recursive: true }); ++ writeFileSync(join(other, COPY_ID, 'packages', 'webui', 'src', 'cli.mjs'), ''); ++ const otherBus = join(dir, 'other-bus'); ++ mkdirSync(otherBus); ++ const r = await run(dir, ['install', ...INSTALL, '--copies', other, '--dir', unitDir, '--no-reload'], standIn(otherBus, '', { bus: busShow(join(other, COPY_ID)) })); ++ assert.equal(r.status, 0, r.stderr); ++ assert.equal(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8'), filled(`--checkout ${REPO} --port 7332`, join(other, COPY_ID))); ++}); ++ ++// The bus accepts only its own human-cli.mjs (packages/bus/src/human.mjs), ++// so a Console on a copy needs its bus on that same copy. install reads the ++// bus unit's effective ExecStart (a drop-in counts) and refuses otherwise. ++test('install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing', async (t) => { ++ const dir = tmp(t); ++ const unitDir = join(dir, 'units'); ++ const copy = copyRoot(dir); ++ const install = (bin, extra = []) => run(dir, ['install', ...INSTALL, ...extra, '--dir', unitDir, '--no-reload'], bin); ++ let n = 0; ++ const sys = (opts, lines = '') => { const d = join(dir, `s${n++}`); mkdirSync(d); return { d, bin: standIn(d, lines, opts) }; }; ++ const refused = (r, line) => { ++ assert.equal(r.status, 1); ++ assert.equal(r.stderr, `webui-service: refusing: ${line}\n`); ++ assert.ok(!existsSync(unitDir)); ++ }; ++ ++ // The bus runs from the checkout, as mosaic-bus@ does today: refused, ++ // with the bus's path and the copy's. ++ let s = sys({ bus: busShow(REPO) }); ++ refused(await install(s.bin), `mosaic-bus@acme.service runs from ${REPO}/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`); ++ // Refused before the Console state is read. ++ assert.ok(!readFileSync(join(s.d, 'calls.txt'), 'utf8').includes('mosaic-webui')); ++ // The same with --business, which names the one unit to read. ++ s = sys({ bus: busShow(REPO) }); ++ refused(await install(s.bin, ['--business', 'acme']), `mosaic-bus@acme.service runs from ${REPO}/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`); ++ assert.equal(readFileSync(join(s.d, 'calls.txt'), 'utf8'), '--user show mosaic-bus@acme.service -p LoadState -p ExecStart\n'); ++ // A path that only starts with the copy's name is not inside it. ++ s = sys({ bus: busShow(`${copy}-old`) }); ++ refused(await install(s.bin), `mosaic-bus@acme.service runs from ${copy}-old/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`); ++ // Two loaded bus units: each must run from the copy. ++ s = sys({ busUnits: `${BUS_UNITS}mosaic-bus@other.service loaded active running Mosaic bus host (other)\n` }); ++ assert.equal((await install(s.bin)).status, 0); ++ assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@other\.service -p LoadState -p ExecStart/); ++ rmSync(unitDir, { recursive: true }); ++ ++ // No bus unit installed for the business: refused. ++ s = sys({ bus: 'LoadState=not-found\nExecStart=\n' }); ++ refused(await install(s.bin, ['--business', 'acme']), `no bus unit mosaic-bus@acme.service is installed (LoadState not-found); the Console on the copy ${copy} needs its bus on that copy`); ++ // No --business and no mosaic-bus@ unit loaded: refused. ++ s = sys({ busUnits: '' }); ++ refused(await install(s.bin), `no mosaic-bus@ unit is loaded, and the Console on the copy ${copy} needs its bus on that copy; name the business with --business ID`); ++ // A loaded unit with no ExecStart path: refused. ++ s = sys({ bus: 'LoadState=loaded\nExecStart=\n' }); ++ refused(await install(s.bin), 'mosaic-bus@acme.service has no ExecStart path, so where its bus runs from is unknown; nothing was changed'); ++ // A failed systemctl call: one line, nothing changed. ++ s = sys({ busFails: true }); ++ refused(await install(s.bin), 'systemctl --user list-units --all --no-legend --plain mosaic-bus@*.service failed, so the bus unit is unknown; nothing was changed'); ++ s = sys({ busFails: true }); ++ refused(await install(s.bin, ['--business', 'acme']), 'systemctl --user show mosaic-bus@acme.service failed, so the bus unit is unknown; nothing was changed'); ++ ++ // The bus runs from the same copy: accepted, with or without --business. ++ for (const extra of [[], ['--business', 'acme']]) { ++ s = sys({ bus: busShow(copy) }); ++ const r = await install(s.bin, extra); ++ assert.equal(r.status, 0, r.stderr); ++ assert.ok(existsSync(join(unitDir, 'mosaic-webui.service'))); ++ rmSync(unitDir, { recursive: true }); ++ } ++}); ++ ++test('render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged', async (t) => { ++ const dir = tmp(t); ++ const r = await run(dir, ['render', '--copy', COPY_ID, ...PREVIEW, '--send']); ++ assert.equal(r.status, 0, r.stderr); ++ assert.equal(r.stdout, filled(`--checkout ${REPO} --port 7332 ${PREVIEW_FLAGS} --send`, copyRoot(dir))); ++ assert.match(r.stdout, new RegExp(`^WorkingDirectory=${esc(copyRoot(dir))}$`, 'm')); ++ assert.match(r.stdout, new RegExp(`^ExecStart=\\S+ ${esc(copyRoot(dir))}/packages/webui/src/cli\\.mjs serve --checkout ${esc(REPO)} `, 'm')); ++ const plain = await run(dir, ['render', ...PREVIEW]); ++ assert.equal(plain.stdout, filled(`--port 7332 ${PREVIEW_FLAGS}`)); ++ assert.ok(!execStart(plain.stdout).includes('--checkout')); ++ // render only prints: it takes no --allow-worktree and needs no canonical checkout. ++ assert.equal((await run(dir, ['render', '--copy', 'f'.repeat(40)])).status, 1); ++}); ++ ++test('the uid header takes only an Authentik uid; the username header still takes a username', async (t) => { ++ const dir = tmp(t); ++ const unitDir = join(dir, 'units'); ++ const head = ['--public-host', 'console.example.com', '--auth-header']; ++ for (const header of ['X-authentik-uid', 'x-authentik-uid', 'X-AUTHENTIK-UID', 'x-AUTHENTIK-uid']) { ++ const ok = await run(dir, ['render', ...head, header, '--allow-user', UID]); ++ assert.equal(ok.status, 0, `${header}: ${ok.stderr}`); ++ for (const user of [UID.slice(1), `${UID}0`, UID.toUpperCase(), `${UID.slice(0, 63)}g`, 'jason', 'jason@example.com']) { ++ for (const verb of [['render'], ['install', ...INSTALL, '--dir', unitDir, '--no-reload']]) { ++ const r = await run(dir, [...verb, ...head, header, '--allow-user', user]); ++ assert.equal(r.status, 1, `${verb[0]} ${header} ${user}`); ++ assert.equal(r.stderr, `webui-service: with --auth-header ${header} the allowed user must be an Authentik uid, 64 lower-case hex characters (^[0-9a-f]{64}$, lead decision 101): ${user}\n`); ++ } ++ } ++ } ++ assert.ok(!existsSync(unitDir), 'no refused install wrote a unit'); ++ for (const user of ['jason', 'jason@example.com', UID]) { ++ const r = await run(dir, ['render', ...head, 'X-authentik-username', '--allow-user', user]); ++ assert.equal(r.status, 0, `${user}: ${r.stderr}`); ++ assert.ok(execStart(r.stdout).endsWith(` --allow-user ${user}`)); ++ } ++}); ++ ++test('a preview value cannot start with - or .', async (t) => { ++ const dir = tmp(t); ++ const cases = [ ++ [['--public-host', '-x.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', UID], 'public host must be a hostname, no port or scheme: -x.example.com'], ++ [['--public-host', '.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', UID], 'public host must be a hostname, no port or scheme: .example.com'], ++ [['--public-host', 'console.example.com', '--auth-header', '-X-authentik-uid', '--allow-user', UID], 'auth header must be a header name: -X-authentik-uid'], ++ [['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '-jason'], 'allowed user must start with a letter or digit: -jason'], ++ [['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '.jason'], 'allowed user must start with a letter or digit: .jason'], ++ [['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--port'], 'allowed user must start with a letter or digit: --port'], ++ ]; ++ for (const [args, refusal] of cases) { ++ const r = await run(dir, ['render', ...args]); ++ assert.equal(r.status, 1, args.join(' ')); ++ assert.equal(r.stderr, `webui-service: ${refusal}\n`); ++ } ++}); ++ ++test('status names the copy and whether refactor has moved past it, and says when the unit runs from no copy', async (t) => { ++ const dir = tmp(t); ++ const unitDir = join(dir, 'units'); ++ const target = join(unitDir, 'mosaic-webui.service'); ++ const bin = standIn(mkdtempSync(join(dir, 'si-'))); ++ const port = await closedPort(); ++ const head = await new Promise((resolve, reject) => { ++ const git = spawn('git', ['-C', REPO, 'rev-parse', '--verify', 'refs/heads/refactor']); ++ let out = ''; ++ git.stdout.on('data', (d) => { out += d; }); ++ git.on('close', (code) => (code === 0 ? resolve(out.trim()) : reject(new Error('no refs/heads/refactor')))); ++ }); ++ // status only reads the unit file, so a hand-written one stands in. ++ mkdirSync(unitDir); ++ const unit = (wd) => writeFileSync(target, `[Service]\nWorkingDirectory=${wd}\nExecStart=/usr/bin/node ${wd}/packages/webui/src/cli.mjs serve --port ${port}\n`); ++ unit(join(dir, 'copies', head)); ++ let r = await run(dir, ['status', '--dir', unitDir], bin); ++ assert.equal(r.status, 0, r.stderr); ++ assert.match(r.stdout, new RegExp(`^copy: ${head}\nrefactor: ${head}, the copy's commit$`, 'm')); ++ unit(join(dir, 'copies', COPY_ID)); ++ r = await run(dir, ['status', '--dir', unitDir], bin); ++ assert.match(r.stdout, new RegExp(`^copy: ${COPY_ID}\nrefactor: ${head}, another commit; the copy is ${COPY_ID}$`, 'm')); ++ unit(REPO); ++ r = await run(dir, ['status', '--dir', unitDir], bin); ++ assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(REPO)}$`, 'm')); ++ assert.ok(!/^refactor:/m.test(r.stdout)); ++ // status reads; it calls nothing that changes the unit. ++ assert.deepEqual(readFileSync(join(dir, readdirSync(dir).find((d) => d.startsWith('si-')), 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service( |$)/.test(c)), []); ++}); +diff --git a/scripts/webui-service.sh b/scripts/webui-service.sh +index 944bb97a..b330e5cf 100755 +--- a/scripts/webui-service.sh ++++ b/scripts/webui-service.sh +@@ -1,18 +1,44 @@ + #!/usr/bin/env bash +-# `scripts/webui-service.sh render|install|uninstall|status`: the systemd +-# user unit that runs the Console, `node packages/webui/src/cli.mjs serve` +-# (lead decision 94). The template is ++# `scripts/webui-service.sh copy|render|install|uninstall|status`: the ++# systemd user unit that runs the Console, `node packages/webui/src/cli.mjs ++# serve` (lead decision 94), from a read-only copy of one landed commit ++# (row 77, #1571, lead decision 112). The template is + # packages/webui/systemd/mosaic-webui.service.in; the rendered unit is + # mosaic-webui.service in the systemd user directory, one instance. + # +-# render [FLAGS] print the rendered unit on stdout +-# install [--dir DIR] [--no-reload] [FLAGS] ++# copy COMMIT [--copies DIR] [--allow-worktree] ++# write `git archive` of the paths the Console ++# loads at COMMIT into DIR//, read-only; ++# DIR defaults to ++# ${XDG_DATA_HOME:-~/.local/share}/mosaic-console ++# render [--copy ID [--copies DIR]] [FLAGS] ++# print the rendered unit on stdout; without ++# --copy it runs from this checkout ++# install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS] + # write the unit (write to a temp file, then +-# rename) and run `systemctl --user daemon-reload` ++# rename) and run `systemctl --user daemon-reload`; ++# refuses while the unit is running + # uninstall [--dir DIR] [--no-reload] +-# remove the unit; refuses while it is active +-# status [--dir DIR] unit state, whether it sends, then GET +-# /healthz on its port ++# remove the unit; refuses while it is running ++# status [--dir DIR] unit state, the copy it runs and whether ++# refactor has moved past it, whether it sends, ++# then GET /healthz on its port ++# ++# With --copy the unit's WorkingDirectory and ExecStart point at the copy, ++# and ExecStart passes this checkout as serve's --checkout: the queue read ++# runs the checkout's queue-read.mjs, since the queue store refuses code ++# outside the canonical checkout; everything else comes from the copy. ++# ++# copy and install run only from the canonical checkout, whose root holds a ++# .git directory (a linked worktree has a .git file). --allow-worktree lifts ++# that refusal and exists for the tests only, which run in worktrees. ++# ++# The update step: `copy COMMIT`; Jason or Mos stops the unit; `install ++# --copy ID` with the same flags; Jason or Mos starts it. Rollback is the ++# same with the earlier id. Copies stay until someone removes them. install ++# refuses unless the bus unit runs from the same copy (bus_on_copy below), ++# and nothing puts the bus on a copy yet, so the step can't be used until a ++# later row does. + # + # FLAGS are serve's: --port N (default 7332; 7330 is the operator's own + # Console), --business ID, --board URL, and the preview set --public-host +@@ -29,19 +55,27 @@ + # decision 102, which accepts that sends from a Console Mos started enter + # the trail as Jason (human-cli). Any other agent starting it, even detached + # through systemd, works around the human proof (lead decisions 62 and 94). ++# install and uninstall refuse while the unit is running, with or without ++# --no-reload: a unit rewritten under a running Console would take the new ++# ExecStart at its next Restart=on-failure, with nobody starting it. + # + # Exit codes: 0 ok, 1 operation failed, 4 usage. + set -euo pipefail + REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in" + UNIT_NAME="mosaic-webui.service" +-USAGE="usage: scripts/webui-service.sh render [FLAGS] | install [--dir DIR] [--no-reload] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR] +- FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]" ++USAGE="usage: scripts/webui-service.sh copy COMMIT [--copies DIR] [--allow-worktree] | render [--copy ID [--copies DIR]] [FLAGS] | install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR] ++ FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]] ++ --allow-worktree is for the tests only: copy and install otherwise refuse outside the canonical checkout." + + die() { echo "webui-service: $*" >&2; exit 1; } + usage() { echo "$USAGE" >&2; exit 4; } + + UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" ++COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-console" ++COPY_ID="" ++COPY_FLAGS=0 ++ALLOW_WORKTREE=0 + RELOAD=1 + DIR_FLAGS=0 + PORT=7332 +@@ -57,6 +91,9 @@ parse_flags() { + while [ $# -gt 0 ]; do + case "$1" in + --dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; DIR_FLAGS=1; shift 2 ;; ++ --copy) [ $# -ge 2 ] || usage; COPY_ID="$2"; COPY_FLAGS=1; shift 2 ;; ++ --copies) [ $# -ge 2 ] || usage; COPIES="$2"; COPY_FLAGS=1; shift 2 ;; ++ --allow-worktree) ALLOW_WORKTREE=1; shift ;; + --no-reload) RELOAD=0; DIR_FLAGS=1; shift ;; + --port) [ $# -ge 2 ] || usage; PORT="$2"; SERVE_SET=1; shift 2 ;; + --business) [ $# -ge 2 ] || usage; BUSINESS="$2"; SERVE_SET=1; shift 2 ;; +@@ -102,13 +139,69 @@ serve_flags() { + case "$PUBLIC_HOST" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac + case "$AUTH_HEADER" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $AUTH_HEADER" ;; esac + case "$AUTH_HEADER" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $AUTH_HEADER" ;; esac ++ case "$ALLOW_USER" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $ALLOW_USER" ;; esac + case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac ++ # Lead decision 101: the uid header carries the Authentik uid, 64 ++ # lower-case hex characters, and nothing else is let through on it. ++ if [ "${AUTH_HEADER,,}" = x-authentik-uid ] && ! [[ "$ALLOW_USER" =~ ^[0-9a-f]{64}$ ]]; then ++ die "with --auth-header $AUTH_HEADER the allowed user must be an Authentik uid, 64 lower-case hex characters (^[0-9a-f]{64}\$, lead decision 101): $ALLOW_USER" ++ fi + flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER" + fi + [ "$SEND" = 0 ] || flags="$flags --send" + echo "$flags" + } + ++# The paths the Console loads, the only ones a copy holds: serve's import ++# closure (packages/webui/src/cli.mjs and the human transport child, ++# packages/bus/src/human-cli.mjs) reaches these nine src directories ++# (packages/tasks/src since row 61: reads.mjs imports its field table ++# loader, which reads packages/tasks/fields.json); ++# serve runs scripts/mosaic-config.mjs as its config check and reads roles/ ++# and RELEASE; the unit's Documentation= names the README. The queue read ++# runs from the checkout (serve --checkout), so packages/queue is not here. ++# The pinned-copy 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. ++COPY_PATHS=( ++ packages/bus/src ++ packages/business/src ++ packages/cli/src ++ packages/conversation/src ++ packages/discord/src ++ packages/harness/src ++ packages/seat/src ++ packages/webui/src ++ packages/webui/README.md ++ packages/tasks/src ++ packages/tasks/fields.json ++ scripts/mosaic-config.mjs ++ roles ++ RELEASE ++) ++ ++# N2 (row 75 review): copy and install read and point at this checkout, so ++# they run from the canonical one, never a worktree someone may remove. ++canonical() { ++ [ -d "$REPO/.git" ] || [ "$ALLOW_WORKTREE" = 1 ] || die "refusing: $REPO is not the canonical checkout (its .git is not a directory); run copy and install from the canonical checkout" ++} ++ ++# The copies directory goes into the unit as part of the copy's path. ++copies_ok() { ++ case "$COPIES" in /*) ;; *) die "copies directory must be an absolute path: $COPIES" ;; esac ++ case "$COPIES" in *[!A-Za-z0-9/._+-]*) die "copies directory contains a character the unit cannot carry: $COPIES" ;; esac ++} ++ ++# The root the unit runs from: the copy named by --copy, else this checkout. ++unit_root() { ++ if [ -z "$COPY_ID" ]; then echo "$REPO"; return 0; fi ++ case "$COPY_ID" in *[!0-9a-f]*) die "--copy must be a full 40-character commit id: $COPY_ID" ;; esac ++ [ "${#COPY_ID}" -eq 40 ] || die "--copy must be a full 40-character commit id: $COPY_ID" ++ copies_ok ++ [ -f "$COPIES/$COPY_ID/packages/webui/src/cli.mjs" ] || die "no copy $COPY_ID in $COPIES; make it with: scripts/webui-service.sh copy $COPY_ID" ++ echo "$COPIES/$COPY_ID" ++} ++ + render() { + [ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE" + local node +@@ -119,18 +212,68 @@ render() { + case "$node" in /*) ;; *) die "node path must be absolute: $node" ;; esac + case "$node" in *[!A-Za-z0-9/._+-]*) die "node path contains a character the unit cannot carry: $node" ;; esac + case "$REPO" in *[!A-Za-z0-9/._+-]*) die "repository path contains a character the unit cannot carry: $REPO" ;; esac ++ local root ++ root="$(unit_root)" || exit 1 + local node_dir path="/usr/local/bin:/usr/bin:/bin" + node_dir="$(dirname "$node")" + case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac + local flags + flags="$(serve_flags)" || exit 1 +- sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE" ++ [ -z "$COPY_ID" ] || flags="--checkout $REPO $flags" ++ sed -e "s|@REPO@|$root|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE" ++} ++ ++# The unit's active state from list-units, printed when it is anything but ++# inactive or failed (active, activating, deactivating, reloading...). A ++# unit systemd has not loaded is not listed and prints nothing. N4: a ++# failed call refuses with one line naming it; the state is unknown, so ++# nothing is written or removed. ++running() { ++ local out ++ out="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null)" || die "refusing: systemctl --user list-units --no-legend --plain $UNIT_NAME failed, so the unit's state is unknown; nothing was changed" ++ printf '%s\n' "$out" | awk -v unit="$UNIT_NAME" '$1 == unit && $3 != "inactive" && $3 != "failed" {print $3}' ++} ++ ++# The bus host proves a human by checking that the calling process runs its ++# own packages/bus/src/human-cli.mjs (packages/bus/src/human.mjs), and the ++# Console spawns the one beside its own files. So a Console on a copy works ++# only with a bus on that same copy: install refuses unless every bus unit ++# it would call has an ExecStart path inside the copy. With --business the ++# unit is mosaic-bus@ID.service; without it, every mosaic-bus@ unit systemd ++# has loaded, and none loaded refuses. Like the state read, a failed call ++# refuses with one line. ++bus_on_copy() { ++ local copy="$1" units unit out load paths p ++ if [ -n "$BUSINESS" ]; then ++ units="mosaic-bus@$BUSINESS.service" ++ else ++ out="$(systemctl --user list-units --all --no-legend --plain 'mosaic-bus@*.service' 2>/dev/null)" || die "refusing: systemctl --user list-units --all --no-legend --plain mosaic-bus@*.service failed, so the bus unit is unknown; nothing was changed" ++ units="$(printf '%s\n' "$out" | awk '$1 ~ /^mosaic-bus@.+\.service$/ {print $1}')" ++ [ -n "$units" ] || die "refusing: no mosaic-bus@ unit is loaded, and the Console on the copy $copy needs its bus on that copy; name the business with --business ID" ++ fi ++ for unit in $units; do ++ out="$(systemctl --user show "$unit" -p LoadState -p ExecStart 2>/dev/null)" || die "refusing: systemctl --user show $unit failed, so the bus unit is unknown; nothing was changed" ++ load="$(printf '%s\n' "$out" | sed -n 's/^LoadState=//p')" ++ [ "$load" = loaded ] || die "refusing: no bus unit $unit is installed (LoadState ${load:-unknown}); the Console on the copy $copy needs its bus on that copy" ++ paths="$(printf '%s\n' "$out" | sed -n 's/^ExecStart=//p' | grep -o 'path=[^ ;]*' | sed 's/^path=//' || true)" ++ [ -n "$paths" ] || die "refusing: $unit has no ExecStart path, so where its bus runs from is unknown; nothing was changed" ++ for p in $paths; do ++ case "$p" in "$copy"/*) ;; *) die "refusing: $unit runs from $p, not the copy $copy; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy" ;; esac ++ done ++ done + } + + install_unit() { + parse_flags "$@" ++ canonical ++ [ -n "$COPY_ID" ] || die "install needs --copy ID, a copy made with: scripts/webui-service.sh copy COMMIT" + local unit + unit="$(render)" || exit 1 ++ bus_on_copy "$COPIES/$COPY_ID" ++ # N1: a running unit would take the new ExecStart at its next restart. ++ local state ++ state="$(running)" || exit 1 ++ [ -z "$state" ] || die "refusing: $UNIT_NAME is running ($state); Jason or Mos stops it first (systemctl --user stop mosaic-webui), then install, then start it" + mkdir -p "$UNIT_DIR" + local target="$UNIT_DIR/$UNIT_NAME" tmp + tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")" +@@ -163,13 +306,12 @@ MSG + + uninstall_unit() { + parse_flags "$@" +- [ "$SERVE_SET" = 0 ] || usage ++ [ "$SERVE_SET" = 0 ] && [ "$COPY_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage + local target="$UNIT_DIR/$UNIT_NAME" +- if [ "$RELOAD" = 1 ]; then +- local active +- active="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')" +- [ -z "$active" ] || die "refusing: $UNIT_NAME is still running; the human stops it first (systemctl --user stop mosaic-webui)" +- fi ++ # N3: checked with --no-reload too, which skips only daemon-reload. ++ local state ++ state="$(running)" || exit 1 ++ [ -z "$state" ] || die "refusing: $UNIT_NAME is still running ($state); Jason or Mos stops it first (systemctl --user stop mosaic-webui)" + if [ -f "$target" ]; then + rm -f "$target" + echo "removed: $target" +@@ -184,7 +326,7 @@ uninstall_unit() { + + status_unit() { + parse_flags "$@" +- [ "$SERVE_SET" = 0 ] && [ "$RELOAD" = 1 ] || usage ++ [ "$SERVE_SET" = 0 ] && [ "$RELOAD" = 1 ] && [ "$COPY_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage + local target="$UNIT_DIR/$UNIT_NAME" + echo "unit: $UNIT_NAME" + echo " enabled: $(systemctl --user is-enabled "$UNIT_NAME" 2>&1 || true)" +@@ -194,8 +336,26 @@ status_unit() { + echo "healthz: not checked; no unit at $target" + return 0 + fi +- # render puts --send right after the allowed user, which may itself be +- # the value --send; a unit that sends ends with both. ++ # The copy is the last part of WorkingDirectory; status only reads. ++ local wd copy head ++ wd="$(sed -n 's/^WorkingDirectory=//p' "$target")" ++ copy="${wd##*/}" ++ if [[ "$copy" =~ ^[0-9a-f]{40}$ ]]; then ++ echo "copy: $copy" ++ if head="$(git -C "$REPO" rev-parse --verify --quiet refs/heads/refactor 2>/dev/null)"; then ++ if [ "$head" = "$copy" ]; then ++ echo "refactor: $head, the copy's commit" ++ else ++ echo "refactor: $head, another commit; the copy is $copy" ++ fi ++ else ++ echo "refactor: unknown; git rev-parse refs/heads/refactor failed in $REPO" ++ fi ++ else ++ echo "copy: none; the unit runs from $wd" ++ fi ++ # render puts --send right after the allowed user. The allowed user ++ # starts with a letter or digit (N5), so it is never the value --send. + if grep -q '^ExecStart=.* --allow-user [^ ][^ ]* --send$' "$target"; then + echo "send: on (--send; human messages to roles)" + else +@@ -212,12 +372,51 @@ fetch(process.argv[1], { signal: AbortSignal.timeout(5000) }) + ' "$url")" + } + ++# Writes the copy into a temp directory beside the target, takes write ++# permission off every file, then renames it into place, so a copy at its ++# final path is always whole and read-only. An existing copy is left alone. ++copy_commit() { ++ [ $# -ge 1 ] || usage ++ local commit="$1"; shift ++ case "$commit" in -*) usage ;; esac ++ parse_flags "$@" ++ [ "$SERVE_SET" = 0 ] && [ "$DIR_FLAGS" = 0 ] && [ -z "$COPY_ID" ] || usage ++ canonical ++ copies_ok ++ case "$commit" in [A-Za-z0-9]*) ;; *) die "commit must be a commit id or branch name: $commit" ;; esac ++ case "$commit" in *[!A-Za-z0-9._/-]*) die "commit must be a commit id or branch name: $commit" ;; esac ++ git -C "$REPO" rev-parse --verify --quiet refs/heads/refactor >/dev/null || die "no refs/heads/refactor in $REPO" ++ local id ++ id="$(git -C "$REPO" rev-parse --verify --quiet "$commit^{commit}")" || die "not a commit: $commit" ++ git -C "$REPO" merge-base --is-ancestor "$id" refs/heads/refactor || die "refusing: $id is not reachable from refs/heads/refactor" ++ local target="$COPIES/$id" ++ if [ -e "$target" ]; then echo "exists: $target"; return 0; fi ++ mkdir -p "$COPIES" ++ COPY_TMP="$(mktemp -d "$COPIES/.$id.XXXXXX")" ++ trap 'chmod -R u+w "$COPY_TMP" 2>/dev/null; rm -rf "$COPY_TMP"' EXIT ++ git -C "$REPO" archive --format=tar "$id" -- "${COPY_PATHS[@]}" | tar -x -f - -C "$COPY_TMP" || die "git archive of $id failed; no copy written" ++ # Read-only files and directories: a writer that replaces by rename ++ # (git, editors, sed -i) can't change a copy either. ++ find "$COPY_TMP" -type f -exec chmod a-w {} + ++ find "$COPY_TMP" -type d -exec chmod 0555 {} + ++ if ! mv -T "$COPY_TMP" "$target" 2>/dev/null; then ++ [ -e "$target" ] || die "could not rename the copy into $target" ++ echo "exists: $target" ++ return 0 ++ fi ++ trap - EXIT ++ echo "copied: $target" ++ echo "install it with: scripts/webui-service.sh install --copy $id, with the unit's flags" ++} ++ + [ $# -ge 1 ] || usage + cmd="$1"; shift + case "$cmd" in ++ copy) copy_commit "$@" ;; + render) + parse_flags "$@" +- [ "$DIR_FLAGS" = 0 ] || usage ++ [ "$DIR_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage ++ { [ -n "$COPY_ID" ] || [ "$COPY_FLAGS" = 0 ]; } || usage + render ;; + install) install_unit "$@" ;; + uninstall) uninstall_unit "$@" ;; diff --git a/agents/rocko/work/queue-77/r2/candidate-manifest.sha256 b/agents/rocko/work/queue-77/r2/candidate-manifest.sha256 new file mode 100644 index 000000000..1f1d632a8 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/candidate-manifest.sha256 @@ -0,0 +1,10 @@ +28c4708c062b752832576eb42aa1f1c9cede16f940d6d8929a4f47957c2f2b4d agents/rocko/work/queue-77/byte-check.sh +85f722a82873b52d2c02f8573eb9c63b025514f239551bff214e44b8303b00c0 agents/rocko/work/queue-77/r2/mutants.py +683aa2c5ee05404c08c35f13113c80a8029fe7881bab8dd8083ba3c87c261815 docs/TOOLS.md +063e1d6c492f32d80d71e669f537622d2e920d7dbfb37f3cfaafeb59d753ea0c packages/webui/README.md +fdd202e6faa08f3b6e1784b805c6e4db5464126b8d7ed54f471cc38a524597e6 packages/webui/src/cli.mjs +b41a2670df325112efe9292667131edee2b6ac307e3bfc9e5b03d531171ea06d packages/webui/src/reads.mjs +7a449ef91310ab6b0185ab97f1720e0790adbd9a8d1d83c3c716e36da976acb5 packages/webui/systemd/mosaic-webui.service.in +9f336ae0e51814bc19fcd5a039ca95dc92912663c6a46eaf63e1933d6c84e863 packages/webui/tests/pinned-copy.test.mjs +3fd286a744e7e4827b2f15da9bcae1484228ba077c4823ac84bfad9026e42470 packages/webui/tests/service.test.mjs +03350df28932e61c16a3ba87db4ccd2d3cff8e74a88fb08f5828a462639aa185 scripts/webui-service.sh diff --git a/agents/rocko/work/queue-77/r2/files.txt b/agents/rocko/work/queue-77/r2/files.txt new file mode 100644 index 000000000..51a9b8bfe --- /dev/null +++ b/agents/rocko/work/queue-77/r2/files.txt @@ -0,0 +1,10 @@ +agents/rocko/work/queue-77/byte-check.sh +agents/rocko/work/queue-77/r2/mutants.py +docs/TOOLS.md +packages/webui/README.md +packages/webui/src/cli.mjs +packages/webui/src/reads.mjs +packages/webui/systemd/mosaic-webui.service.in +packages/webui/tests/pinned-copy.test.mjs +packages/webui/tests/service.test.mjs +scripts/webui-service.sh diff --git a/agents/rocko/work/queue-77/r2/mutants.py b/agents/rocko/work/queue-77/r2/mutants.py new file mode 100644 index 000000000..cd69b83c3 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/mutants.py @@ -0,0 +1,53 @@ +# Row 77 mutants (rounds 1 and 2): each edit applied alone to , the service and pinned-copy +# tests run, the file restored. Usage: python3 mutants.py +import subprocess, shutil, sys +W=sys.argv[1] # the tree with the candidate applied +S='scripts/webui-service.sh' +M=[ + ('failed counts as running', S, '&& $3 != "failed" {', '{'), + ('reloading not running', S, '$3 != "failed" {', '$3 != "failed" && $3 != "reloading" {'), + ('list-units failure ignored', S, '2>/dev/null)" || die "refusing: systemctl --user list-units', '2>/dev/null)" || true; : "refusing: systemctl --user list-units'), + ('install skips state read', S, ' state="$(running)" || exit 1\n [ -z "$state" ] || die "refusing: $UNIT_NAME is running', ' state=""\n [ -z "$state" ] || die "refusing: $UNIT_NAME is running'), + ('uninstall skips state read with --no-reload', S, '[ -z "$state" ] || die "refusing: $UNIT_NAME is still running', '[ -z "$state" ] || [ "$RELOAD" = 0 ] || die "refusing: $UNIT_NAME is still running'), + ('uid header case-sensitive', S, '"${AUTH_HEADER,,}" = x-authentik-uid', '"$AUTH_HEADER" = X-authentik-uid'), + ('uid allows upper hex', S, '^[0-9a-f]{64}$ ]]; then', '^[0-9a-fA-F]{64}$ ]]; then'), + ('allowed user may start with -', S, 'case "$ALLOW_USER" in [A-Za-z0-9]*) ;;', 'case "$ALLOW_USER" in *) ;;'), + ('no --checkout in unit', S, 'flags="--checkout $REPO $flags"', 'flags="$flags"'), + ('canonical always passes', S, '[ -d "$REPO/.git" ] || [ "$ALLOW_WORKTREE" = 1 ] ||', 'true ||'), + ('install without --copy', S, '[ -n "$COPY_ID" ] || die "install needs --copy ID', 'true || die "install needs --copy ID'), + ('missing copy accepted', S, '[ -f "$COPIES/$COPY_ID/packages/webui/src/cli.mjs" ] ||', 'true ||'), + ('status drift wrong', S, 'if [ "$head" = "$copy" ]; then', 'if true; then'), + ('copy misses seat', S, ' packages/seat/src\n', ''), + ('copy misses README', S, ' packages/webui/README.md\n', ''), + ('commit may start with a dot', S, 'case "$commit" in [A-Za-z0-9]*) ;;', 'case "$commit" in [.A-Za-z0-9]*) ;;'), + ('copy misses tasks src', S, ' packages/tasks/src\n', ''), + ('copy misses fields table', S, ' packages/tasks/fields.json\n', ''), + ('copy stays writable', S, '-type f -exec chmod a-w', '-type f -exec true'), + ('copy dirs stay writable', S, '-type d -exec chmod 0555', '-type d -exec true'), + ('copy subdirs stay writable', S, '-type d -exec chmod 0555 {} +', '-maxdepth 0 -type d -exec chmod 0555 {} +'), + ('trap leaves read-only temp', S, "trap 'chmod -R u+w \"$COPY_TMP\" 2>/dev/null; rm -rf", "trap 'rm -rf"), + ('copy off-branch accepted', S, 'merge-base --is-ancestor "$id" refs/heads/refactor ||', 'true ||'), + ('queue reads root', 'packages/webui/src/reads.mjs', 'root: queueRoot', 'root'), + ('--checkout relative ok', 'packages/webui/src/cli.mjs', 'isAbsolute(', '!!('), + ('install skips bus check', S, ' unit="$(render)" || exit 1\n bus_on_copy', ' unit="$(render)" || exit 1\n : bus_on_copy'), + ('bus path prefix only', S, 'in "$copy"/*) ;;', 'in "$copy"*) ;;'), + ('bus not-found accepted', S, '[ "$load" = loaded ] ||', 'true ||'), + ('bus show failure ignored', S, '-p LoadState -p ExecStart 2>/dev/null)" || die', '-p LoadState -p ExecStart 2>/dev/null)" || true; : die'), + ('bus list failure ignored', S, "'mosaic-bus@*.service' 2>/dev/null)\" || die", "'mosaic-bus@*.service' 2>/dev/null)\" || true; : die"), + ('no loaded bus accepted', S, '[ -n "$units" ] || die', 'true || die'), + ('no ExecStart path accepted', S, '[ -n "$paths" ] || die', 'true || die'), + ('only first bus unit checked', S, ' for unit in $units; do', ' for unit in ${units%%[[:space:]]*}; do'), + ('--business ignored for bus', S, ' if [ -n "$BUSINESS" ]; then\n units=', ' if false; then\n units='), +] +res=[] +for name,f,a,b in M: + p=f'{W}/{f}'; s=open(p).read() + if s.count(a)<1: res.append(f'NOMATCH {name}'); continue + shutil.copy(p,p+'.bak'); open(p,'w').write(s.replace(a,b)) + try: + r=subprocess.run(['node','--test','tests/service.test.mjs','tests/pinned-copy.test.mjs'],cwd=f'{W}/packages/webui',capture_output=True,text=True,timeout=300) + fails=[l for l in r.stdout.splitlines() if l.startswith('✖') and 'failing tests' not in l] + res.append(f"{'KILLED' if r.returncode else 'SURVIVED'} {name}: {len(set(fails))} failing") + finally: + shutil.move(p+'.bak',p) +print('\n'.join(res)) diff --git a/agents/rocko/work/queue-77/r2/out/after-gate.sh b/agents/rocko/work/queue-77/r2/out/after-gate.sh new file mode 100755 index 000000000..72e4dba68 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/after-gate.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Row 77 round 2, after the gate: the webui suite again, bus.test.mjs alone +# five times (the gate's one failure), the mutants on the rebased candidate, +# and the byte check against the base. Sequential, output kept. +W=/mnt/storage/scratch/rocko-r77 +O=/mnt/storage/scratch/rocko-r77-r2/gate +P=/mnt/storage/scratch/rocko-r77-r2/pkt +cd "$W" || exit 1 +node --test 'packages/webui/tests/*.test.mjs' > "$O/node-webui-rerun.txt" 2>&1; echo "node-webui rerun exit $?" | tee -a "$O/after.txt" +grep -E '^ℹ (pass|fail)' "$O/node-webui-rerun.txt" | tee -a "$O/after.txt" +for i in 1 2 3 4 5; do + node --test packages/webui/tests/bus.test.mjs > "$O/bus-test-$i.txt" 2>&1; echo "bus.test.mjs run $i exit $?" | tee -a "$O/after.txt" +done +uptime | tee -a "$O/after.txt" +python3 "$P/mutants.py" "$W" > "$P/mutants.txt" 2>&1; echo "mutants: $(grep -c '^KILLED' "$P/mutants.txt") killed, $(grep -c '^SURVIVED' "$P/mutants.txt") survived" | tee -a "$O/after.txt" +bash /mnt/storage/src/mosaic-stack/agents/rocko/work/queue-77/byte-check.sh "$W" "$(cat "$P/base.txt")" "$P/byte-check" > "$P/byte-check.txt" 2>&1; echo "byte-check exit $?" | tee -a "$O/after.txt" diff --git a/agents/rocko/work/queue-77/r2/out/byte-check.txt b/agents/rocko/work/queue-77/r2/out/byte-check.txt new file mode 100644 index 000000000..6df0f3aed --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check.txt @@ -0,0 +1,10 @@ +template body: same +1 exit 0/0 same: render +2 exit 0/0 same: render --port 7444 +3 exit 0/0 same: render --business mosaic --board http://127.0.0.1:7331 +4 exit 0/0 same: render --public-host console.example.com --auth-header X-authentik-uid --allow-user 6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e +5 exit 0/0 same: render --public-host console.example.com --auth-header X-authentik-uid --allow-user 6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e --send +6 exit 0/0 same: render --public-host console.example.com --auth-header X-authentik-username --allow-user jason +7 exit 1/1 same: render --port 0 +8 exit 0/0 same: render --business a.b +8 of 8 same diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-1.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-1.txt new file mode 100644 index 000000000..2035c1ff6 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-1.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-2.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-2.txt new file mode 100644 index 000000000..dd66470fd --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-2.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7444 +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-3.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-3.txt new file mode 100644 index 000000000..758862cfe --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-3.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --business mosaic --board http://127.0.0.1:7331 +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-4.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-4.txt new file mode 100644 index 000000000..0f9a7aa46 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-4.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --public-host console.example.com --auth-header X-authentik-uid --allow-user 6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-5.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-5.txt new file mode 100644 index 000000000..bec656528 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-5.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --public-host console.example.com --auth-header X-authentik-uid --allow-user 6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e --send +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-6.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-6.txt new file mode 100644 index 000000000..6d5b84cbd --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-6.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --public-host console.example.com --auth-header X-authentik-username --allow-user jason +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-7.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-7.txt new file mode 100644 index 000000000..6a4dc77d8 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-7.txt @@ -0,0 +1 @@ +webui-service: port must be 1..65535: 0 diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/base-8.txt b/agents/rocko/work/queue-77/r2/out/byte-check/base-8.txt new file mode 100644 index 000000000..4b5518c80 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/base-8.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --business a.b +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-1.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-1.txt new file mode 100644 index 000000000..2035c1ff6 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-1.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-2.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-2.txt new file mode 100644 index 000000000..dd66470fd --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-2.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7444 +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-3.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-3.txt new file mode 100644 index 000000000..758862cfe --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-3.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --business mosaic --board http://127.0.0.1:7331 +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-4.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-4.txt new file mode 100644 index 000000000..0f9a7aa46 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-4.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --public-host console.example.com --auth-header X-authentik-uid --allow-user 6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-5.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-5.txt new file mode 100644 index 000000000..bec656528 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-5.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --public-host console.example.com --auth-header X-authentik-uid --allow-user 6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e --send +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-6.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-6.txt new file mode 100644 index 000000000..6d5b84cbd --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-6.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --public-host console.example.com --auth-header X-authentik-username --allow-user jason +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-7.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-7.txt new file mode 100644 index 000000000..6a4dc77d8 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-7.txt @@ -0,0 +1 @@ +webui-service: port must be 1..65535: 0 diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/cand-8.txt b/agents/rocko/work/queue-77/r2/out/byte-check/cand-8.txt new file mode 100644 index 000000000..4b5518c80 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/byte-check/cand-8.txt @@ -0,0 +1,20 @@ +[Unit] +Description=Mosaic Console +Documentation=file:///mnt/storage/scratch/rocko-r77/packages/webui/README.md +StartLimitIntervalSec=600 +StartLimitBurst=5 + +[Service] +Type=simple +WorkingDirectory=/mnt/storage/scratch/rocko-r77 +Environment=PATH=/usr/local/bin:/usr/bin:/bin +ExecStart=/usr/bin/node /mnt/storage/scratch/rocko-r77/packages/webui/src/cli.mjs serve --port 7332 --business a.b +Restart=on-failure +RestartSec=15 +RestartPreventExitStatus=2 +KillSignal=SIGTERM +TimeoutStopSec=30 +NoNewPrivileges=yes + +[Install] +WantedBy=default.target diff --git a/agents/rocko/work/queue-77/r2/out/byte-check/template-body.diff b/agents/rocko/work/queue-77/r2/out/byte-check/template-body.diff new file mode 100644 index 000000000..e69de29bb diff --git a/agents/rocko/work/queue-77/r2/out/gate.sh b/agents/rocko/work/queue-77/r2/out/gate.sh new file mode 100755 index 000000000..d349a88dd --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Row 77 gate: webui node suite, then every scripts/test-*.sh, sequentially, full output kept. +W=/mnt/storage/scratch/rocko-r77 +O=/mnt/storage/scratch/rocko-r77-r2/gate +mkdir -p "$O" +cd "$W" || exit 1 +node --test 'packages/webui/tests/*.test.mjs' > "$O/node-webui.txt" 2>&1; echo "node-webui exit $?" | tee -a "$O/summary.txt" +grep -E '^ℹ (pass|fail)' "$O/node-webui.txt" | tee -a "$O/summary.txt" +for s in scripts/test-*.sh; do + n=$(basename "$s" .sh) + bash "$s" > "$O/$n.txt" 2>&1; echo "$n exit $?" | tee -a "$O/summary.txt" + tail -3 "$O/$n.txt" | sed "s/^/ $n: /" | tee -a "$O/summary.txt" +done +grep -l -E '✖|^not ok' "$O"/*.txt | sed 's/^/failure markers in: /' | tee -a "$O/summary.txt" diff --git a/agents/rocko/work/queue-77/r2/out/gate/after.txt b/agents/rocko/work/queue-77/r2/out/gate/after.txt new file mode 100644 index 000000000..34a56c971 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/after.txt @@ -0,0 +1,11 @@ +node-webui rerun exit 0 +ℹ pass 99 +ℹ fail 0 +bus.test.mjs run 1 exit 0 +bus.test.mjs run 2 exit 0 +bus.test.mjs run 3 exit 0 +bus.test.mjs run 4 exit 0 +bus.test.mjs run 5 exit 0 + 14:52:03 up 36 days, 16:26, 6 users, load average: 17.19, 15.86, 17.62 +mutants: 34 killed, 0 survived +byte-check exit 0 diff --git a/agents/rocko/work/queue-77/r2/out/gate/bus-test-1.txt b/agents/rocko/work/queue-77/r2/out/gate/bus-test-1.txt new file mode 100644 index 000000000..c5237ccd2 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/bus-test-1.txt @@ -0,0 +1,18 @@ +✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (161.249345ms) +✔ decisions route serves the roles and recent lists from the reader view (53.662611ms) +✔ credentials route serves the broker credential status from the reader view (18.226566ms) +✔ replies route serves the replies to one message from the reader view (42.604654ms) +✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (72.24404ms) +✔ bus refusals map to statuses and never carry a path or a stack (30.764775ms) +✔ humanCall speaks the human transport protocol without blocking the server (1764.61973ms) +✔ busReader takes --business, else the live bus host, else refuses (258.396508ms) +✔ humanCall kills a transport that runs past its timeout (530.540486ms) +✔ serve refuses a --business value that is not a business id (391.31184ms) +ℹ tests 10 +ℹ suites 0 +ℹ pass 10 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3590.111513 diff --git a/agents/rocko/work/queue-77/r2/out/gate/bus-test-2.txt b/agents/rocko/work/queue-77/r2/out/gate/bus-test-2.txt new file mode 100644 index 000000000..c9ecd713d --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/bus-test-2.txt @@ -0,0 +1,18 @@ +✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (94.426818ms) +✔ decisions route serves the roles and recent lists from the reader view (49.074979ms) +✔ credentials route serves the broker credential status from the reader view (12.568848ms) +✔ replies route serves the replies to one message from the reader view (37.691656ms) +✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (65.872606ms) +✔ bus refusals map to statuses and never carry a path or a stack (30.962879ms) +✔ humanCall speaks the human transport protocol without blocking the server (1762.475645ms) +✔ busReader takes --business, else the live bus host, else refuses (317.111924ms) +✔ humanCall kills a transport that runs past its timeout (529.670013ms) +✔ serve refuses a --business value that is not a business id (301.086504ms) +ℹ tests 10 +ℹ suites 0 +ℹ pass 10 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3383.957406 diff --git a/agents/rocko/work/queue-77/r2/out/gate/bus-test-3.txt b/agents/rocko/work/queue-77/r2/out/gate/bus-test-3.txt new file mode 100644 index 000000000..1193664e5 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/bus-test-3.txt @@ -0,0 +1,18 @@ +✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (86.799967ms) +✔ decisions route serves the roles and recent lists from the reader view (34.778475ms) +✔ credentials route serves the broker credential status from the reader view (10.3248ms) +✔ replies route serves the replies to one message from the reader view (38.894225ms) +✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (78.307086ms) +✔ bus refusals map to statuses and never carry a path or a stack (51.771651ms) +✔ humanCall speaks the human transport protocol without blocking the server (1774.138607ms) +✔ busReader takes --business, else the live bus host, else refuses (263.177027ms) +✔ humanCall kills a transport that runs past its timeout (559.303281ms) +✔ serve refuses a --business value that is not a business id (810.128012ms) +ℹ tests 10 +ℹ suites 0 +ℹ pass 10 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3853.216345 diff --git a/agents/rocko/work/queue-77/r2/out/gate/bus-test-4.txt b/agents/rocko/work/queue-77/r2/out/gate/bus-test-4.txt new file mode 100644 index 000000000..8a288b17a --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/bus-test-4.txt @@ -0,0 +1,18 @@ +✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (147.576748ms) +✔ decisions route serves the roles and recent lists from the reader view (71.24427ms) +✔ credentials route serves the broker credential status from the reader view (24.686191ms) +✔ replies route serves the replies to one message from the reader view (65.144473ms) +✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (96.434519ms) +✔ bus refusals map to statuses and never carry a path or a stack (35.40309ms) +✔ humanCall speaks the human transport protocol without blocking the server (1857.011131ms) +✔ busReader takes --business, else the live bus host, else refuses (500.647909ms) +✔ humanCall kills a transport that runs past its timeout (531.323287ms) +✔ serve refuses a --business value that is not a business id (467.071352ms) +ℹ tests 10 +ℹ suites 0 +ℹ pass 10 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3983.488051 diff --git a/agents/rocko/work/queue-77/r2/out/gate/bus-test-5.txt b/agents/rocko/work/queue-77/r2/out/gate/bus-test-5.txt new file mode 100644 index 000000000..63f06c311 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/bus-test-5.txt @@ -0,0 +1,18 @@ +✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (131.308066ms) +✔ decisions route serves the roles and recent lists from the reader view (73.717113ms) +✔ credentials route serves the broker credential status from the reader view (26.525262ms) +✔ replies route serves the replies to one message from the reader view (43.755167ms) +✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (87.245157ms) +✔ bus refusals map to statuses and never carry a path or a stack (31.51965ms) +✔ humanCall speaks the human transport protocol without blocking the server (1789.459657ms) +✔ busReader takes --business, else the live bus host, else refuses (358.189276ms) +✔ humanCall kills a transport that runs past its timeout (532.381089ms) +✔ serve refuses a --business value that is not a business id (545.94619ms) +ℹ tests 10 +ℹ suites 0 +ℹ pass 10 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3812.185455 diff --git a/agents/rocko/work/queue-77/r2/out/gate/node-webui-rerun.txt b/agents/rocko/work/queue-77/r2/out/gate/node-webui-rerun.txt new file mode 100644 index 000000000..bb4f2bb92 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/node-webui-rerun.txt @@ -0,0 +1,108 @@ +✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (7922.129029ms) +Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286} +✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (10589.292103ms) +✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (13846.049306ms) +✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (272.984809ms) +✔ decisions route serves the roles and recent lists from the reader view (284.339931ms) +✔ credentials route serves the broker credential status from the reader view (54.853255ms) +✔ replies route serves the replies to one message from the reader view (208.806256ms) +✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (480.022848ms) +✔ bus refusals map to statuses and never carry a path or a stack (320.875093ms) +✔ humanCall speaks the human transport protocol without blocking the server (4504.413901ms) +✔ busReader takes --business, else the live bus host, else refuses (1086.858268ms) +✔ humanCall kills a transport that runs past its timeout (538.572787ms) +✔ serve refuses a --business value that is not a business id (1014.827467ms) +✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (7634.87859ms) +✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (4494.326139ms) +✔ conversation view: a newer session with no readable history keeps the marker (2208.366445ms) +✔ conversation view: seats without history say so and offer no reply (1468.715575ms) +✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (6641.979629ms) +✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (58409.064883ms) +✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (3123.713189ms) +✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (4399.658862ms) +✔ copy writes nothing when an archived path is missing at the commit (1455.180147ms) +✔ a copy that loses the rename race leaves no read-only temp directory behind (1344.95328ms) +✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (2876.584159ms) +✔ serve refuses a --checkout that is not an absolute directory path (738.777325ms) +✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (100.933236ms) +✔ preview options are all three or none, and each is checked (71.416393ms) +✔ the CLI refuses a partial or invalid preview flag set at start (5925.258645ms) +✔ the CLI with all three flags serves the preview mode (1120.846329ms) +✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (83.167076ms) +✔ the header name matches without regard to case, and the value compare is exact (48.60573ms) +✔ the public name with the header reads the bus views, and the Host and Origin are checked first (180.376949ms) +✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (58.779701ms) +✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (10.558133ms) +✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (46.371305ms) +✔ a repeated Host is 400 before any other check, in preview mode and without it (38.70273ms) +✔ without the preview flags the public name is refused and writes still reach the board (25.624988ms) +✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.618182ms) +✔ credential state from the date alone, as the bus states it (0.390239ms) +✔ queue: rows, one row, ids and methods, notes; no path or id from a row (4828.447275ms) +✔ queue: a refused read fails closed with the store's text, redacted (2195.360083ms) +✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (3945.207078ms) +✔ business: names, vars on the allowlist, authority, credential metadata only (23.99685ms) +✔ business: missing or invalid file refuses with the module's text, redacted (24.215847ms) +✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (71.843937ms) +✔ settings: release, board, loopback address, notifier binding name only (1035.907629ms) +✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (72.91973ms) +✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (7058.779135ms) +✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (25687.574783ms) +✔ the send box: held roles, the draft and caret across a refresh, each receipt, one POST per Send, and the trail link; the board Reply on a 4xx and a 5xx (9571.866523ms) +✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (13447.203331ms) +✔ startServer takes send only as a function and only with the preview mode (38.494842ms) +✔ the CLI refuses --send without the preview flags, and without a configured bus (3189.296308ms) +✔ the CLI with --send and a configured bus sends through the human transport and logs one line (2077.584188ms) +✔ without --send the route is read-only like every other write, and config says no send (89.8573ms) +✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (105.842042ms) +✔ config says send only on the public name with --send on (31.210357ms) +✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (35.525904ms) +✔ each request check refuses before anything is sent, and logs the refusal without the body (250.517286ms) +✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (13.355596ms) +✔ a crafted role name cannot forge a log line (7.595144ms) +✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (34.977451ms) +✔ through the human transport the request is exactly the business, message.send and the keys (699.379015ms) +✔ against a real broker: the message is written once as a human send, and its refusals map (282.651102ms) +✔ loopback host and board origin fail closed (26.574479ms) +✔ real board fixture passes through WebUI; assets and isolated seen/reply work (692.385365ms) +✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (157.697756ms) +✔ unreachable board reports URL; CLI rejects unsupported options (5188.488208ms) +✔ render prints the unit without the preview flags (177.83225ms) +✔ render carries every serve flag, the preview set included (75.348573ms) +✔ a partial preview set is refused (225.778763ms) +✔ a value the unit cannot carry is refused, for every flag (4118.666824ms) +✔ the rendered unit names none of the human proof agent markers and sets no agent variable (343.383428ms) +✔ install into --dir with --no-reload writes the file whole, then reports unchanged (508.407268ms) +✔ install without --no-reload runs daemon-reload and nothing that starts the unit (251.227001ms) +✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (1193.671278ms) +✔ status prints the unit state and GET /healthz on the installed port (406.049224ms) +✔ usage: an unknown verb or flag, and start or enable, exit 4 (116.166834ms) +✔ without --send, render is the template with its placeholders filled, byte for byte (122.991269ms) +✔ render --send with the preview set ends ExecStart with --send and changes nothing else (164.049609ms) +✔ --send without the whole preview set is refused and writes nothing (258.177359ms) +✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (1067.692317ms) +✔ uninstall and status take no --send (98.845504ms) +✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (799.309273ms) +✔ when the state read fails, install and uninstall refuse with one line and change nothing (122.206697ms) +✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (109.620424ms) +✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (614.213943ms) +✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (32.644275ms) +✔ the uid header takes only an Authentik uid; the username header still takes a username (616.204805ms) +✔ a preview value cannot start with - or . (68.907765ms) +✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (347.958796ms) +✔ tokens.css and icons.svg are byte copies of docs/design (22.800193ms) +✔ JetBrains Mono files match the digests in their sources file, with the OFL text (3.63211ms) +✔ shipped strings are unchanged, and the web keeps its write boundary (4.060874ms) +✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (11089.882591ms) +✔ five states on Board, Inbox, Tasks, Agents and Trail (2958.939696ms) +✔ Queue, Business and Settings in their five states, with no move button (7588.987121ms) +✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (2711.388576ms) +✔ the server log names no config path when the system config refuses (473.55308ms) +ℹ tests 99 +ℹ suites 0 +ℹ pass 99 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 59395.123456 diff --git a/agents/rocko/work/queue-77/r2/out/gate/node-webui.txt b/agents/rocko/work/queue-77/r2/out/gate/node-webui.txt new file mode 100644 index 000000000..93a9648e9 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/node-webui.txt @@ -0,0 +1,129 @@ +✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (13353.062302ms) +Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286} +✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (17505.077679ms) +✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (21181.360792ms) +✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (514.383962ms) +✔ decisions route serves the roles and recent lists from the reader view (266.757958ms) +✔ credentials route serves the broker credential status from the reader view (58.699559ms) +✔ replies route serves the replies to one message from the reader view (247.282253ms) +✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (675.981643ms) +✔ bus refusals map to statuses and never carry a path or a stack (235.703397ms) +✖ humanCall speaks the human transport protocol without blocking the server (3361.135662ms) +✔ busReader takes --business, else the live bus host, else refuses (2944.918922ms) +✔ humanCall kills a transport that runs past its timeout (779.122407ms) +✔ serve refuses a --business value that is not a business id (2753.024234ms) +✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (12928.611006ms) +✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (5813.12294ms) +✔ conversation view: a newer session with no readable history keeps the marker (3096.668897ms) +✔ conversation view: seats without history say so and offer no reply (2649.397758ms) +✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (11186.080048ms) +✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (53058.506895ms) +✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (3644.410381ms) +✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (9728.174492ms) +✔ copy writes nothing when an archived path is missing at the commit (3970.533696ms) +✔ a copy that loses the rename race leaves no read-only temp directory behind (1362.906942ms) +✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (3048.775691ms) +✔ serve refuses a --checkout that is not an absolute directory path (999.704484ms) +✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (587.347684ms) +✔ preview options are all three or none, and each is checked (17.22235ms) +✔ the CLI refuses a partial or invalid preview flag set at start (8329.855ms) +✔ the CLI with all three flags serves the preview mode (2086.394744ms) +✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (276.985812ms) +✔ the header name matches without regard to case, and the value compare is exact (196.690926ms) +✔ the public name with the header reads the bus views, and the Host and Origin are checked first (243.570901ms) +✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (729.074769ms) +✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (11.857127ms) +✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (130.317131ms) +✔ a repeated Host is 400 before any other check, in preview mode and without it (35.076658ms) +✔ without the preview flags the public name is refused and writes still reach the board (118.636348ms) +✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (3.571251ms) +✔ credential state from the date alone, as the bus states it (0.458354ms) +✔ queue: rows, one row, ids and methods, notes; no path or id from a row (6200.374865ms) +✔ queue: a refused read fails closed with the store's text, redacted (4773.698702ms) +✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (7089.238058ms) +✔ business: names, vars on the allowlist, authority, credential metadata only (53.019466ms) +✔ business: missing or invalid file refuses with the module's text, redacted (43.157373ms) +✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (55.083594ms) +✔ settings: release, board, loopback address, notifier binding name only (692.232526ms) +✔ task fields: the committed table with no reads configured, GET only; a bad table refuses with no path (265.343821ms) +✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (13140.177462ms) +✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (30875.11132ms) +✔ the send box: held roles, the draft and caret across a refresh, each receipt, one POST per Send, and the trail link; the board Reply on a 4xx and a 5xx (17686.470718ms) +✔ a sent receipt lists its replies as the page refreshes, oldest first and capped, and marks none delivered or read (14649.112647ms) +✔ startServer takes send only as a function and only with the preview mode (83.297459ms) +✔ the CLI refuses --send without the preview flags, and without a configured bus (6801.340148ms) +✔ the CLI with --send and a configured bus sends through the human transport and logs one line (2992.652988ms) +✔ without --send the route is read-only like every other write, and config says no send (151.048723ms) +✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (101.145607ms) +✔ config says send only on the public name with --send on (23.158872ms) +✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (137.394777ms) +✔ each request check refuses before anything is sent, and logs the refusal without the body (233.684497ms) +✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (66.595528ms) +✔ a crafted role name cannot forge a log line (50.322395ms) +✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (59.646091ms) +✔ through the human transport the request is exactly the business, message.send and the keys (1184.981285ms) +✔ against a real broker: the message is written once as a human send, and its refusals map (942.91515ms) +✔ loopback host and board origin fail closed (32.398348ms) +✔ real board fixture passes through WebUI; assets and isolated seen/reply work (612.495128ms) +✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (375.783908ms) +✔ unreachable board reports URL; CLI rejects unsupported options (6974.571769ms) +✔ render prints the unit without the preview flags (527.087753ms) +✔ render carries every serve flag, the preview set included (314.620671ms) +✔ a partial preview set is refused (1017.24355ms) +✔ a value the unit cannot carry is refused, for every flag (5937.492565ms) +✔ the rendered unit names none of the human proof agent markers and sets no agent variable (307.249806ms) +✔ install into --dir with --no-reload writes the file whole, then reports unchanged (957.169485ms) +✔ install without --no-reload runs daemon-reload and nothing that starts the unit (451.676692ms) +✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (1694.11373ms) +✔ status prints the unit state and GET /healthz on the installed port (927.078831ms) +✔ usage: an unknown verb or flag, and start or enable, exit 4 (593.176179ms) +✔ without --send, render is the template with its placeholders filled, byte for byte (626.057374ms) +✔ render --send with the preview set ends ExecStart with --send and changes nothing else (634.790175ms) +✔ --send without the whole preview set is refused and writes nothing (1047.147522ms) +✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (1416.030944ms) +✔ uninstall and status take no --send (292.176366ms) +✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (3165.689542ms) +✔ when the state read fails, install and uninstall refuse with one line and change nothing (1008.734269ms) +✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (646.89939ms) +✔ install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing (1106.818942ms) +✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (69.968863ms) +✔ the uid header takes only an Authentik uid; the username header still takes a username (1280.511271ms) +✔ a preview value cannot start with - or . (89.433601ms) +✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (475.249886ms) +✔ tokens.css and icons.svg are byte copies of docs/design (3.3454ms) +✔ JetBrains Mono files match the digests in their sources file, with the OFL text (26.377882ms) +✔ shipped strings are unchanged, and the web keeps its write boundary (9.230331ms) +✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (14604.156031ms) +✔ five states on Board, Inbox, Tasks, Agents and Trail (6696.544258ms) +✔ Queue, Business and Settings in their five states, with no move button (11775.027569ms) +✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (5054.620031ms) +✔ the server log names no config path when the system config refuses (1573.285308ms) +ℹ tests 99 +ℹ suites 0 +ℹ pass 98 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 54658.862666 + +✖ failing tests: + +test at packages/webui/tests/bus.test.mjs:266:1 +✖ humanCall speaks the human transport protocol without blocking the server (3361.135662ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + 'outcome-unknown' + - 'invalid-response' + + at TestContext. (file:///mnt/storage/scratch/rocko-r77/packages/webui/tests/bus.test.mjs:273:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'outcome-unknown', + expected: 'invalid-response', + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/rocko/work/queue-77/r2/out/gate/summary.txt b/agents/rocko/work/queue-77/r2/out/gate/summary.txt new file mode 100644 index 000000000..d2466e721 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/summary.txt @@ -0,0 +1,40 @@ +node-webui exit 1 +ℹ pass 98 +ℹ fail 1 +test-auth exit 0 + test-auth: OK auth.sh without valid config refuses + test-auth: + test-auth: selftest: 15 passed, 0 failed +test-conductor exit 0 + test-conductor: OK invalid policy exits 2 (exit 2) + test-conductor: + test-conductor: selftest: 17 passed, 0 failed +test-config exit 0 + test-config: OK failed validation modified nothing + test-config: + test-config: selftest: 24 passed, 0 failed +test-discord exit 0 + test-discord: OK service install with USER unset finishes and names the account for lingering + test-discord: + test-discord: discord suite: 66 passed, 0 failed +test-extension-package exit 0 + test-extension-package: OK nested second entrypoint fails closed + test-extension-package: + test-extension-package: extension package selftest: 18 passed, 0 failed +test-foundation exit 0 + test-foundation: OK no field of the non-bundle file is echoed + test-foundation: + test-foundation: selftest: 44 passed, 0 failed +test-queue exit 0 + test-queue: skip queue verify and render --check: this checkout (/mnt/storage/scratch/rocko-r77) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) + test-queue: + test-queue: queue suite: 27 passed, 0 failed +test-release exit 0 + test-release: OK rollback without previous refuses (exit 1) + test-release: + test-release: selftest: 14 passed, 0 failed +test-task exit 0 + test-task: OK no agent identity on headless run + test-task: + test-task: selftest: 98 passed, 0 failed +failure markers in: /mnt/storage/scratch/rocko-r77-r2/gate/node-webui.txt diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-auth.txt b/agents/rocko/work/queue-77/r2/out/gate/test-auth.txt new file mode 100644 index 000000000..ac2287606 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-auth.txt @@ -0,0 +1,17 @@ +OK status with missing harness credential exits 3 and still lists accounts +OK status reports harness credential (read-only) + mosaic accounts +OK api key material never reaches output +OK oauth token material never reaches output +OK unparseable credential file exits 2 +OK symlinked credential file exits 4 +OK env-side credential names reported +OK env var values never reach output +OK accounts without an accounts dir reports none and creates nothing +OK accounts lists files and marks the active one +OK loose account perms flagged in listing +OK agent --auth with missing account file refuses (exit 4) +OK agent --auth with non-0600 account file refuses +OK agent --auth with invalid account name refuses +OK auth.sh without valid config refuses + +selftest: 15 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-conductor.txt b/agents/rocko/work/queue-77/r2/out/gate/test-conductor.txt new file mode 100644 index 000000000..62afc513c --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-conductor.txt @@ -0,0 +1,23 @@ +On branch refactor +Your branch is up to date with 'origin/refactor'. + +nothing to commit, working tree clean +OK dry-run: allowed change, exit 0, nothing committed (exit 0) +OK dry-run committed nothing +OK apply: allowed change exits 0 (exit 0) +OK apply: attribution in commit subject +OK apply: target tree clean after commit +OK disallowed path refused (exit 1) +OK disallowed path: target untouched +OK syntax gate refused broken .mjs (exit 1) +OK syntax gate: target untouched +OK suite failure refused (exit 1) +OK suite failure: target reverted to clean +OK disabled policy refused (exit 2) +OK disabled policy: target untouched +OK failed run refused (exit 1) +OK failed run: target untouched +OK missing run exits 4 (exit 4) +OK invalid policy exits 2 (exit 2) + +selftest: 17 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-config.txt b/agents/rocko/work/queue-77/r2/out/gate/test-config.txt new file mode 100644 index 000000000..76c05ef74 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-config.txt @@ -0,0 +1,26 @@ +OK absent adapter defaults to pi +OK adapter mock validates (exit 0) +OK unsupported adapter exits 2 (exit 2) +OK env exports adapter +OK bootstrap creates default when absent (exit 0) +OK bootstrap wrote config file +OK bootstrap is idempotent on existing config (exit 0) +OK bootstrap did not rewrite existing config +OK validate missing config exits 3 (exit 3) +OK malformed JSON exits 2 (exit 2) +OK unsupported configVersion exits 2 (exit 2) +OK unknown top-level key exits 2 (exit 2) +OK unknown execution key exits 2 (exit 2) +OK unsupported backend exits 2 (exit 2) +OK unsupported environment exits 2 (exit 2) +OK relative dataRoot exits 2 (exit 2) +OK non-canonical dataRoot exits 2 (exit 2) +OK filesystem root dataRoot exits 2 (exit 2) +OK home directory dataRoot exits 2 (exit 2) +OK dataRoot containing config dir exits 2 (exit 2) +OK control character in provider exits 2 (exit 2) +OK symlinked config file exits 2 (exit 2) +OK env exports resolve correctly +OK failed validation modified nothing + +selftest: 24 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-discord.txt b/agents/rocko/work/queue-77/r2/out/gate/test-discord.txt new file mode 100644 index 000000000..b013f9ffa --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-discord.txt @@ -0,0 +1,70 @@ +toolchain: node v26.8.1 + +OK syntax: packages/discord/src/approvals.mjs +OK syntax: packages/discord/src/authorize.mjs +OK syntax: packages/discord/src/binding.mjs +OK syntax: packages/discord/src/cli.mjs +OK syntax: packages/discord/src/connector.mjs +OK syntax: packages/discord/src/context.mjs +OK syntax: packages/discord/src/engine-pi.mjs +OK syntax: packages/discord/src/errors.mjs +OK syntax: packages/discord/src/gateway.mjs +OK syntax: packages/discord/src/git.mjs +OK syntax: packages/discord/src/journal.mjs +OK syntax: packages/discord/src/notify.mjs +OK syntax: packages/discord/src/rest.mjs +OK syntax: packages/discord/src/setspark.mjs +OK syntax: packages/discord/src/tools.mjs +OK syntax: packages/discord/src/web.mjs +OK syntax: packages/discord/bin/git-credential.mjs +OK syntax: packages/discord/extension/tools.mjs +OK syntax: packages/discord/tests/approvals.test.mjs +OK syntax: packages/discord/tests/authorize.test.mjs +OK syntax: packages/discord/tests/binding.test.mjs +OK syntax: packages/discord/tests/connector.test.mjs +OK syntax: packages/discord/tests/context.test.mjs +OK syntax: packages/discord/tests/engine.test.mjs +OK syntax: packages/discord/tests/fake-pi.mjs +OK syntax: packages/discord/tests/gateway.test.mjs +OK syntax: packages/discord/tests/git.test.mjs +OK syntax: packages/discord/tests/helpers.mjs +OK syntax: packages/discord/tests/journal.test.mjs +OK syntax: packages/discord/tests/notify.test.mjs +OK syntax: packages/discord/tests/recover.test.mjs +OK syntax: packages/discord/tests/rest.test.mjs +OK syntax: packages/discord/tests/setspark.test.mjs +OK syntax: packages/discord/tests/tools.test.mjs +OK syntax: packages/discord/tests/web.test.mjs +OK syntax: packages/discord/fixtures/claim-worker.mjs +OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs +OK syntax: scripts/discord.sh +OK syntax: scripts/discord-service.sh +OK packages/discord declares no dependencies +OK no bot-token-shaped string in packages/discord +OK fixture binding uses placeholder ids only +OK fixture binding validates +OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool +OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start +OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root +OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token +OK real pi with protocol vault adds reserve_id to the git verbs +OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key +OK real pi refuses a git key on a read-only root (fail closed) +OK real pi with the pilot flags (--no-tools) exposes no tool at all +OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed) +OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT +OK node --test packages/discord/tests/ (ℹ pass 178) +OK scripts/discord.sh --help exits 0 +OK scripts/discord.sh check without a binding exits 4 +OK scripts/discord.sh recover without a binding exits 4 +OK scripts/discord.sh reload without a binding exits 4 +OK scripts/discord-service.sh without a command exits 4 +OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP +OK service install writes the rendered unit (0644) and leaves no temp file +OK service install a second time reports unchanged +OK systemd-analyze verify accepts the rendered unit +OK service uninstall removes the unit file +OK service install with an unknown flag exits 4 +OK service install with USER unset finishes and names the account for lingering + +discord suite: 66 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-extension-package.txt b/agents/rocko/work/queue-77/r2/out/gate/test-extension-package.txt new file mode 100644 index 000000000..69c352e10 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-extension-package.txt @@ -0,0 +1,21 @@ +OK initial ordinary-file install +OK installed tree matches canonical source +OK installed tree has no symlinks +OK check detects installation drift +OK sync refuses to overwrite installation drift +OK check detects an extra destination file +OK check detects an extra destination directory +OK check rejects a destination symlink +OK sync accepts a canonical source update +OK updated installation matches canonical source +scripts/test-extension-package.sh: line 14: 3599819 Killed "$@" > /dev/null 2>&1 +OK forced interruption kills the replacing process +OK next invocation recovers old consistent installation +OK interrupted replacement rolled back +OK sync succeeds after interruption recovery +OK unlocked stale lock file does not block +OK active lock refuses a concurrent sync +OK source symlink fails closed +OK nested second entrypoint fails closed + +extension package selftest: 18 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-foundation.txt b/agents/rocko/work/queue-77/r2/out/gate/test-foundation.txt new file mode 100644 index 000000000..6338e1e6d --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-foundation.txt @@ -0,0 +1,53 @@ +toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0 + +OK syntax: scripts/foundation-inspect.mjs +OK syntax: scripts/foundation/strict-json.mjs +OK syntax: scripts/foundation/canonical.mjs +OK syntax: scripts/foundation/resolve.mjs +OK syntax: scripts/foundation/validate-record.mjs +OK syntax: scripts/foundation/fixtures/build-fixtures.mjs +OK syntax: scripts/foundation/canonical.test.mjs +OK syntax: scripts/foundation/cli.test.mjs +OK syntax: scripts/foundation/fixtures.test.mjs +OK syntax: scripts/foundation/resolve.test.mjs +OK syntax: scripts/foundation/strict-json.test.mjs +OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written) +OK fixture generator runs +OK checked-in fixtures/bundles equal a fresh generation +OK checked-in fixtures/raw equal a fresh generation +OK checked-in fixtures/index.json equal a fresh generation +OK checked-in demo bundles equal a fresh generation +OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT +OK node --test scripts/foundation/ (ℹ pass 80) +OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof) + platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999) + node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases) + schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10 + profile column (schema-valid records only): profile-valid 510, profile-invalid 30 + profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns +OK oracle: zero schema-column disagreements with the pinned checker +OK oracle: strict-only profile refusals are counted and asserted +OK demo: permitted read preview exits 0 (exit 0) +OK demo: permitted file.change preview exits 0 (exit 0) +OK demo: assignment.change proposal is unresolved (exit 3) (exit 3) +OK demo: revoked registration is refused (exit 3) (exit 3) +OK demo: message is not authority (exit 3) (exit 3) +OK usage: no arguments exits 2 (exit 2) +OK io: missing file exits 4 (exit 4) +OK io: directory exits 4 (exit 4) +OK io: symlink exits 4 (O_NOFOLLOW) (exit 4) +OK bound: oversize fixture exits 2 (exit 2) +OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2) +OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2) +OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0) +OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed +OK text output starts with the disclaimer +OK json output is valid JSON with result allowed and exactly the charter §7 fields +OK json golden matches byte-for-byte +OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal +OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs +OK canary never printed (bundle run and credential-file run) +OK a non-bundle JSON file is refused at the shape gate, not read into output +OK no field of the non-bundle file is echoed + +selftest: 44 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-queue.txt b/agents/rocko/work/queue-77/r2/out/gate/test-queue.txt new file mode 100644 index 000000000..70701bb10 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-queue.txt @@ -0,0 +1,35 @@ +toolchain: node v26.8.1, git version 2.55.0 + +OK syntax: packages/queue/src/cli.mjs +OK syntax: packages/queue/src/errors.mjs +OK syntax: packages/queue/src/io.mjs +OK syntax: packages/queue/src/lock.mjs +OK syntax: packages/queue/src/queue.mjs +OK syntax: packages/queue/src/review.mjs +OK syntax: packages/queue/src/store.mjs +OK syntax: packages/queue/tests/commit.test.mjs +OK syntax: packages/queue/tests/data.test.mjs +OK syntax: packages/queue/tests/dispatch.test.mjs +OK syntax: packages/queue/tests/helpers.mjs +OK syntax: packages/queue/tests/lock.test.mjs +OK syntax: packages/queue/tests/migration.test.mjs +OK syntax: packages/queue/tests/review.test.mjs +OK syntax: packages/queue/tests/store.test.mjs +OK syntax: packages/queue/tests/write.test.mjs +OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs +OK syntax: packages/queue/tests/fixtures/kill-at.mjs +OK syntax: packages/queue/tests/fixtures/lock-child.mjs +OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh +OK syntax: scripts/queue-commit.sh +OK syntax: scripts/git-hooks/pre-commit +OK syntax: scripts/mosaic +OK queue-commit.sh, the guard and scripts/mosaic are executable +OK packages/queue declares no dependencies +ℹ tests 150 +ℹ pass 150 +ℹ fail 0 +OK node --test packages/queue/tests/ +OK scripts/mosaic queue help +skip queue verify and render --check: this checkout (/mnt/storage/scratch/rocko-r77) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) + +queue suite: 27 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-release.txt b/agents/rocko/work/queue-77/r2/out/gate/test-release.txt new file mode 100644 index 000000000..7202b85d4 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-release.txt @@ -0,0 +1,16 @@ +OK valid RELEASE resolves (exit 0) +OK invalid RELEASE exits 1 (exit 1) +OK missing RELEASE exits 1 (exit 1) +OK valid RELEASE leaves image tag consistent with version +OK status safe on empty state (exit 0) +OK status created no pointer +OK fault-injected activation refuses (exit 1) +OK refused activation wrote no pointer +OK refusal logged exactly once with valid fields +OK healthy activation succeeds (exit 0) +OK pointer written with valid fields +OK repeat activation succeeds (log grows) (exit 0) +OK log is append-only across activations +OK rollback without previous refuses (exit 1) + +selftest: 14 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/gate/test-task.txt b/agents/rocko/work/queue-77/r2/out/gate/test-task.txt new file mode 100644 index 000000000..df6724a77 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/gate/test-task.txt @@ -0,0 +1,101 @@ +OK valid task validates (exit 0) +OK unknown task key exits 2 (exit 2) +OK unsupported taskVersion exits 2 (exit 2) +OK invalid task id exits 2 (exit 2) +OK empty prompt exits 2 (exit 2) +OK NUL in expectExact exits 2 (exit 2) +OK out-of-range timeout exits 2 (exit 2) +OK missing mission file exits 4 (exit 4) +OK task with valid mission validates (exit 0) +OK invalid mission exits 2 (exit 2) +OK validate missing task exits 4 (exit 4) +OK validation does not modify the task file +OK prune dry-run exits 0 (exit 0) +OK dry-run deleted nothing +OK prune --keep=2 --yes removes oldest (exit 0) +OK kept exactly 2 newest runs +OK newest run kept, oldest pruned +OK append-only receipt written (3 entries) +OK sessions/workspaces untouched by prune +OK prune with invalid keep exits 4 (exit 4) +OK mock adapter: gate passes on matching mock response (exit 0) +OK mock adapter: expect-mismatch recorded (exit 1) +OK mismatch reason recorded +OK unknown adapter fails closed (exit 1) +OK mission task runs via mock adapter (exit 0) +OK mission section injected into generated prompt +OK retry of mission run succeeds (exit 0) +OK retriedFrom lineage recorded +OK mission section present after retry (relative path resolved) +OK retry of missing run exits 4 (exit 4) +OK skill install bundled ms-tools (exit 0) +OK installed to skills-available +OK double install refuses (exit 1) +OK activate enables skill (exit 0) +OK enabled dir populated +OK uninstall while enabled refuses (exit 1) +OK deactivate moves back to available (exit 0) +OK uninstall removes from available (exit 0) +OK seat with enabled skill launches +OK skill path delivered to adapter +OK shipped researcher contract resolves (ceiling + network) +OK resolve-role refuses a non-role document (conductor policy) (exit 2) +OK resolve-role refuses name/filename mismatch (exit 2) +OK resolve-role refuses unknown network declaration (exit 2) +OK resolve-role refuses duplicate role tool (exit 2) +OK resolve-role refuses unsupported tool (exit 2) +OK resolve-role refuses missing contract file (exit 4) +OK version 1 role prints no contract line +OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer) +OK resolve-role accepts a minimal version 2 role (exit 0) +OK resolve-role refuses an action outside the vocabulary (exit 2) +OK resolve-role refuses a gated-only action in a role file (exit 2) +OK resolve-role refuses a Vikunja verb no role may hold (exit 2) +OK resolve-role refuses a version 2 role whose contract is missing (exit 2) +OK resolve-role refuses a contract that is a symbolic link (exit 2) +OK seat launches under role ceiling +OK role ceiling narrows seat tools (read,write,bash -> read,bash) +OK narrowing recorded loudly +OK missing role contract refuses launch (exit 2) +OK missing-contract refusal names the role +OK empty ceiling intersection -> tool-free seat +OK tool-free outcome recorded loudly +OK overridden agents dir without seat definition refuses (exit 4) +OK seat-resolution refusal names the seat +OK policy: mission only -> mission tools +OK policy: task only -> task tools +OK policy: both -> intersection (task narrowed) +OK policy: empty intersection -> tool-free +OK invalid mission capabilities rejected (exit 2) +mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls) +OK task run with user layer present (exit 0) +OK user context dispatched into generated prompt +OK workspace+tools task runs via mock (exit 0) +OK workspace path + tools delivered to adapter +OK persistent workspace created on host +OK plain task still runs (no workspace/tools) (exit 0) +OK workspace var present but empty when absent +OK tools empty when absent +OK unknown tool exits 2 (exit 2) +OK workspace traversal exits 2 (exit 2) +OK live hello task succeeds with exact marker +OK result.json written in run dir +OK result.json contents are correct +OK wrong expectExact fails with exit 1 (reason: expect-mismatch) +OK each run gets a distinct run dir (no clobber) +OK list shows both runs +OK fork base: teach succeeds (exit 0) +OK fork child recalls ancestor context (exit 0) +OK forked child recalled ancestor code word +OK ancestor session untouched by fork +OK child session dir has its own branch file +OK onboard without name exits 4 (non-interactive) (exit 4) +OK onboard --name renders profile (exit 0) +OK profile written +OK canon structure: required filled, optional placeholdered +OK canon sections present +OK user recall run succeeds +OK recalled user name (response: Jason) +OK no agent identity on headless run + +selftest: 98 passed, 0 failed diff --git a/agents/rocko/work/queue-77/r2/out/mutants.txt b/agents/rocko/work/queue-77/r2/out/mutants.txt new file mode 100644 index 000000000..53b6e7a0f --- /dev/null +++ b/agents/rocko/work/queue-77/r2/out/mutants.txt @@ -0,0 +1,34 @@ +KILLED failed counts as running: 2 failing +KILLED reloading not running: 2 failing +KILLED list-units failure ignored: 2 failing +KILLED install skips state read: 3 failing +KILLED uninstall skips state read with --no-reload: 1 failing +KILLED uid header case-sensitive: 1 failing +KILLED uid allows upper hex: 1 failing +KILLED allowed user may start with -: 2 failing +KILLED no --checkout in unit: 4 failing +KILLED canonical always passes: 2 failing +KILLED install without --copy: 1 failing +KILLED missing copy accepted: 2 failing +KILLED status drift wrong: 1 failing +KILLED copy misses seat: 1 failing +KILLED copy misses README: 1 failing +KILLED commit may start with a dot: 1 failing +KILLED copy misses tasks src: 1 failing +KILLED copy misses fields table: 2 failing +KILLED copy stays writable: 1 failing +KILLED copy dirs stay writable: 1 failing +KILLED copy subdirs stay writable: 1 failing +KILLED trap leaves read-only temp: 1 failing +KILLED copy off-branch accepted: 5 failing +KILLED queue reads root: 2 failing +KILLED --checkout relative ok: 1 failing +KILLED install skips bus check: 2 failing +KILLED bus path prefix only: 1 failing +KILLED bus not-found accepted: 1 failing +KILLED bus show failure ignored: 1 failing +KILLED bus list failure ignored: 1 failing +KILLED no loaded bus accepted: 1 failing +KILLED no ExecStart path accepted: 1 failing +KILLED only first bus unit checked: 1 failing +KILLED --business ignored for bus: 1 failing diff --git a/agents/rocko/work/queue-77/r2/packet-manifest.sha256 b/agents/rocko/work/queue-77/r2/packet-manifest.sha256 new file mode 100644 index 000000000..fb81b6ff4 --- /dev/null +++ b/agents/rocko/work/queue-77/r2/packet-manifest.sha256 @@ -0,0 +1,45 @@ +125da8fe5a1d1dc2f4c75215fc560e503b4e6bc720e04bda84f4e5e65e49c032 ./base.txt +46e71d49a77f8c579121734237c6163a90bee10c20671b1efb0df754bb6629ff ./BUILD.md +8f983bf4edcd5e245d46685e2d322b536b7b56cce843092cec011840c595b321 ./build.patch +045f0ac8333709fbe7badbed7ca45f29f0d11c0d2aaee952fb1a5b77727c55ad ./candidate-manifest.sha256 +adaee0d7d46742fe5b1eb17d04b75128f80ae5bc9544a3bc4b1de3ce632bb1d7 ./files.txt +85f722a82873b52d2c02f8573eb9c63b025514f239551bff214e44b8303b00c0 ./mutants.py +b1bb6d5ce6672bb0410098cc7e591f857f19d6788945806bd0953101719ec4f7 ./out/after-gate.sh +7236b1386f47d3e88f44c1baeb3cdc3ba4e0d72df6cf57b3ecd7adf24f56dfb4 ./out/byte-check/base-1.txt +619a486a2828f6eab280f8df4dd82735464c02a5aded055137374f6f3c17f9b0 ./out/byte-check/base-2.txt +64d6be0adfe28df72a3b79ede380561ddf18a93afccacc27ed8314d05ce18c7d ./out/byte-check/base-3.txt +3d5a4aac2243cc40acb90f8917372cedc222e354ea09bce939856568735da229 ./out/byte-check/base-4.txt +ffbfc5ee5865a1fd4705f1ae32b1e802c7b68789955a5e3b381fa242781ea63d ./out/byte-check/base-5.txt +ce08c820abdee674b08f09768e6fc56b8a17cfe082829d8c26daa1e456ec1a6f ./out/byte-check/base-6.txt +d38e7cfe67f8045602923579a9087779cc05cb433490ea606132be49ac53a897 ./out/byte-check/base-7.txt +af9d8b17ad1c42d0c881ff9a72673044b0ebc52eb78cf453ee76614b2cc0be72 ./out/byte-check/base-8.txt +7236b1386f47d3e88f44c1baeb3cdc3ba4e0d72df6cf57b3ecd7adf24f56dfb4 ./out/byte-check/cand-1.txt +619a486a2828f6eab280f8df4dd82735464c02a5aded055137374f6f3c17f9b0 ./out/byte-check/cand-2.txt +64d6be0adfe28df72a3b79ede380561ddf18a93afccacc27ed8314d05ce18c7d ./out/byte-check/cand-3.txt +3d5a4aac2243cc40acb90f8917372cedc222e354ea09bce939856568735da229 ./out/byte-check/cand-4.txt +ffbfc5ee5865a1fd4705f1ae32b1e802c7b68789955a5e3b381fa242781ea63d ./out/byte-check/cand-5.txt +ce08c820abdee674b08f09768e6fc56b8a17cfe082829d8c26daa1e456ec1a6f ./out/byte-check/cand-6.txt +d38e7cfe67f8045602923579a9087779cc05cb433490ea606132be49ac53a897 ./out/byte-check/cand-7.txt +af9d8b17ad1c42d0c881ff9a72673044b0ebc52eb78cf453ee76614b2cc0be72 ./out/byte-check/cand-8.txt +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./out/byte-check/template-body.diff +c8865434415cf50faef608ff185e4390ef068b1239c1bdfa8195f7b7ce98f872 ./out/byte-check.txt +87385410957c4fdb782d10d20d0165d24d01ae24a5b56035273907c5e033cf9a ./out/gate/after.txt +abbee45800283182f193e780bbc6ee2f666f0305effb2318aac386461b09f87e ./out/gate/bus-test-1.txt +2399761fe44a30b3cc017b84caa9f703969414f8e77fff193c0be6ac2148bde2 ./out/gate/bus-test-2.txt +84e3879d5b4a219589aede605f905b4a9daf8fa710e9f62e13ab54de5aeb0bb9 ./out/gate/bus-test-3.txt +83a2c8ecad58df5112b1565d8fd8024e8cff230722068defb4777a593b574b78 ./out/gate/bus-test-4.txt +6f705a0619571629cfb7df5b8974ae79ee143e85f0507ad8938f50c3a9f6b746 ./out/gate/bus-test-5.txt +b21fed7b714090f83978b18c30a5aa6a12f08fda55ff53a9bc2275cc67e39bca ./out/gate/node-webui-rerun.txt +02e7914e7621f2d31faf015611d3b0c64686720b89a0b7b1cb9a22db2ecd9139 ./out/gate/node-webui.txt +12167bcacf7e8d5172c2a5a3e2d33050923a2d6a6aee05e68b216be839d54ee5 ./out/gate.sh +6da159300c4c5d834cc9abf729edabe661f15e692ceabd7c079fb2270e27c872 ./out/gate/summary.txt +f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 ./out/gate/test-auth.txt +e1206366f07f3d9029530dcdcb55c0282d3f052adc60fc7fd51642d5d4ec3600 ./out/gate/test-conductor.txt +52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 ./out/gate/test-config.txt +7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe ./out/gate/test-discord.txt +c61a2dafbbdfcde691e656b9d0ba9b62ab8179002fa7fc0f4686900c76124913 ./out/gate/test-extension-package.txt +83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f ./out/gate/test-foundation.txt +0cd57fe625b02fdc477f1aea2c54a9cabcc8af10eb2b53fd6dbbb1eab3b5c3fd ./out/gate/test-queue.txt +fa832794ee35025fb4559d58d01a270f6bff2eb65e863e9075b797f47fa68545 ./out/gate/test-release.txt +ccea37f4ffde1f160dcd1a1864988083a645d974e892065c956b338d0f8c853a ./out/gate/test-task.txt +212135b87a3a39748e8bba8fa528701098343dfacb2630b56e81ec9542471b7c ./out/mutants.txt diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 214362ceb..77ad3b52b 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -780,3 +780,4 @@ are never rewritten or removed; corrections are new entries. 2026-10-12T19:58:50Z | dewey | row 63 (#1556) round 1 review for darkwing | changes, test-only: comment 27847, rev 740 (in sage's af01465c), packet agents/dewey/work/queue-63-review/r1 (7a3e85a3). Candidate ec28d736 tasks 80, harness 59, business 60, bus 95 rc=0; code right, 5 of 10 of my mutants survive on the child re-read, final-read and fallback paths and on located's loop; proof-tests.mjs kills all five. All five build choices agreed. Filbert approved (27846) with the same gap as non-blocking N1. 2026-10-12T19:56:44Z | dewey | correction to my 19:58:50Z row 63 review line | that stamp was typed, not read from the clock; the line was written at about 19:57Z (date -u read 19:56:30Z just before it). Content unchanged. 2026-10-12T19:57:52Z | sage | Q125 (Mos sage-0104 to 0107): Jason's PM test faults to rows | rows 93-95 briefed (#1587-1589, docs/plans/2026-10-12_pm-test-fixes.md), 89/90/92 briefs extended for faults 9, 10 and the session credential, decision 118; Console cause found (works as built, Board is the machine scan, pre-row-81 build, no PM-started messages, cross-origin 302 during sign-in loss); answered Mos (bus restart ends PM runs), order 92,89,95,94,90,91,93 then redeploy; Input needed to Jason via Mos: row 92 credential choice; pushed 5849de76..40067b32. Correction: 6aebcef3 also swept darkwing revs 719-720, its message names only 711-718 +2026-10-12T20:03Z | rocko | row 77 (#1571) candidate round 2 | packet agents/rocko/work/queue-77/r2/ (candidate manifest 045f0ac8, base 04db738a); in-review→in-progress rev 745, in-progress→in-review round 2 rev 746, request rocko-77-review-2 failed rev 747 (no credential; Sage sends as jarvis); gate 9 scripts green, webui 98/1 load flake in bus.test (not in diff) then 99/0; 34/34 mutants killed; live unit untouched