darkwing: row 77 (#1571) review round 1, changes (comment 27801)
Human proof from a copy (bus checks its own human-cli path), row 61's packages/tasks import missing from COPY_PATHS, directory write bits. webui 94 pass; 19 mutants, 16 killed, 3 survivors explained. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
STATIC {"packages/webui/src":4,"packages/business/src":10,"packages/cli/src":9,"packages/seat/src":3,"packages/conversation/src":6,"packages/bus/src":3,"packages/harness/src":3,"packages/discord/src":6}
|
||||||
|
DYNAMIC
|
||||||
|
URLS
|
||||||
|
packages/cli/src/transport.mjs -> packages/bus/src/human-cli.mjs
|
||||||
|
packages/harness/src/bundle.mjs -> packages/harness/src/pi-extension.mjs
|
||||||
|
packages/harness/src/bundle.mjs -> packages/harness/src/claude-gate.mjs
|
||||||
|
packages/harness/src/bundle.mjs -> packages/harness/src/mcp-server.mjs
|
||||||
|
packages/harness/src/bundle.mjs -> packages/harness/src/gate.mjs
|
||||||
|
packages/harness/src/bundle.mjs -> packages/harness/src/tools.mjs
|
||||||
|
packages/cli/src/host.mjs -> packages/bus/src/process.mjs
|
||||||
|
packages/cli/src/host.mjs -> packages/cli/src/notifier-process.mjs
|
||||||
|
packages/discord/src/git.mjs -> packages/discord/bin/git-credential.mjs
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Static import closure only; report dynamic imports and import.meta.url references found in it.
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, resolve, relative } from 'node:path';
|
||||||
|
const root = resolve(process.argv[2]);
|
||||||
|
const seen = new Set(), dyn = [], urls = [];
|
||||||
|
const stack = process.argv.slice(3).map((p) => resolve(root, p));
|
||||||
|
while (stack.length) {
|
||||||
|
const f = stack.pop();
|
||||||
|
if (seen.has(f)) continue;
|
||||||
|
seen.add(f);
|
||||||
|
const src = readFileSync(f, 'utf8').replace(/^\s*\/\/.*$/gm, '');
|
||||||
|
for (const m of src.matchAll(/(?:\bfrom\s*|^\s*import\s+)['"](\.[^'"]+)['"]/gm)) stack.push(resolve(dirname(f), m[1]));
|
||||||
|
for (const m of src.matchAll(/\bimport\s*\(\s*['"](\.[^'"]+)['"]/g)) dyn.push(`${relative(root, f)} -> ${relative(root, resolve(dirname(f), m[1]))}`);
|
||||||
|
for (const m of src.matchAll(/new URL\(\s*['"]([^'"]+)['"]\s*,\s*import\.meta\.url/g)) urls.push(`${relative(root, f)} -> ${relative(root, resolve(dirname(f), m[1])) || '.'}`);
|
||||||
|
}
|
||||||
|
const dirs = {};
|
||||||
|
for (const f of seen) { const d = relative(root, f).split('/').slice(0, 3).join('/'); dirs[d] = (dirs[d] ?? 0) + 1; }
|
||||||
|
console.log('STATIC', JSON.stringify(dirs));
|
||||||
|
console.log('DYNAMIC'); dyn.forEach((x) => console.log(' ', x));
|
||||||
|
console.log('URLS'); urls.forEach((x) => console.log(' ', x));
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Darkwing row 77 review mutants: single-site edits to the candidate in
|
||||||
|
# WORKTREE. Each backs up one file, edits one site with
|
||||||
|
# queue-59-review/r1/mut.mjs (exactly one match, else BAD-SITE), runs the
|
||||||
|
# named webui test files and restores the backup.
|
||||||
|
# Usage: mutants.sh WORKTREE [NAME,...] (TMPDIR must be set; outputs go there)
|
||||||
|
set -u
|
||||||
|
WT="$1" ONLY="${2:-}"
|
||||||
|
MUT="$(cd "$(dirname "$0")/../../queue-59-review/r1" && pwd)/mut.mjs"
|
||||||
|
cd "$WT" || exit 1
|
||||||
|
S=scripts/webui-service.sh CLI=packages/webui/src/cli.mjs RD=packages/webui/src/reads.mjs
|
||||||
|
SVC=tests/service.test.mjs PIN=tests/pinned-copy.test.mjs
|
||||||
|
|
||||||
|
m() { # name file tests from to
|
||||||
|
local name="$1" file="$2" tests="$3" from="$4" to="$5"
|
||||||
|
if [ -n "$ONLY" ] && [[ ",$ONLY," != *",$name,"* ]]; then return; fi
|
||||||
|
cp -- "$file" "$TMPDIR/mut.bak"
|
||||||
|
if ! node "$MUT" "$file" "$from" "$to"; then echo "$name BAD-SITE"; cp -- "$TMPDIR/mut.bak" "$file"; return; fi
|
||||||
|
if (cd packages/webui && node --test $tests > "$TMPDIR/$name.out" 2>&1); then
|
||||||
|
echo "$name SURVIVED"
|
||||||
|
else
|
||||||
|
echo "$name killed: $(grep -E '^✖ ' "$TMPDIR/$name.out" | grep -v 'failing tests' | sort -u | sed 's/ ([0-9.]*ms)$//' | tr '\n' ';')"
|
||||||
|
fi
|
||||||
|
cp -- "$TMPDIR/mut.bak" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# copy
|
||||||
|
m mv-no-T $S $PIN 'if ! mv -T "$COPY_TMP"' 'if ! mv "$COPY_TMP"'
|
||||||
|
m no-file-chmod $S $PIN ' find "$COPY_TMP" -type f -exec chmod a-w {} +
|
||||||
|
' ''
|
||||||
|
m dir-0700 $S $PIN ' chmod 0755 "$COPY_TMP"
|
||||||
|
' ''
|
||||||
|
m no-trap $S $PIN " trap 'rm -rf \"\$COPY_TMP\"' EXIT
|
||||||
|
" ''
|
||||||
|
m no-early-exists $S $PIN ' if [ -e "$target" ]; then echo "exists: $target"; return 0; fi
|
||||||
|
' ''
|
||||||
|
m no-ancestor $S $PIN ' git -C "$REPO" merge-base --is-ancestor "$id" refs/heads/refactor || die "refusing: $id is not reachable from refs/heads/refactor"
|
||||||
|
' ''
|
||||||
|
m copy-no-canonical $S $PIN ' canonical
|
||||||
|
copies_ok
|
||||||
|
' ' copies_ok
|
||||||
|
'
|
||||||
|
# install, render, uninstall
|
||||||
|
m install-no-canonical $S $SVC ' canonical
|
||||||
|
[ -n "$COPY_ID" ]' ' [ -n "$COPY_ID" ]'
|
||||||
|
m install-no-copy-needed $S $SVC ' [ -n "$COPY_ID" ] || die "install needs --copy ID, a copy made with: scripts/webui-service.sh copy COMMIT"
|
||||||
|
' ''
|
||||||
|
m render-no-checkout $S $SVC ' [ -z "$COPY_ID" ] || flags="--checkout $REPO $flags"
|
||||||
|
' ''
|
||||||
|
m failed-is-running $S $SVC ' && $3 != "failed"' ''
|
||||||
|
m silent-state-fail $S $SVC ')" || die "refusing: systemctl' ')" || exit 1 || die "refusing: systemctl'
|
||||||
|
m no-reload-skips-check $S $SVC ' state="$(running)" || exit 1
|
||||||
|
[ -z "$state" ] || die "refusing: $UNIT_NAME is still running' ' state=""; [ "$RELOAD" = 0 ] || state="$(running)" || exit 1
|
||||||
|
[ -z "$state" ] || die "refusing: $UNIT_NAME is still running'
|
||||||
|
m uid-length-range $S $SVC '^[0-9a-f]{64}$ ]]' '^[0-9a-f]{63,65}$ ]]'
|
||||||
|
# status
|
||||||
|
m status-drift-inverted $S $SVC 'if [ "$head" = "$copy" ]; then' 'if [ "$head" != "$copy" ]; then'
|
||||||
|
m status-any-copy $S $SVC 'if [[ "$copy" =~ ^[0-9a-f]{40}$ ]]; then' 'if [ -n "$copy" ]; then'
|
||||||
|
# serve and the reads
|
||||||
|
m queue-root-ignored $RD $PIN 'queueReader({ root: queueRoot,' 'queueReader({ root,'
|
||||||
|
m cli-checkout-ignored $CLI $PIN 'queueRoot: options.checkout ?? REPO' 'queueRoot: REPO'
|
||||||
|
m checkout-relative-ok $CLI $PIN 'dir = isAbsolute(value) && statSync(value).isDirectory();' 'dir = statSync(value).isDirectory();'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
mv-no-T SURVIVED
|
||||||
|
no-file-chmod killed: ✖ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone;
|
||||||
|
dir-0700 SURVIVED
|
||||||
|
no-trap killed: ✖ copy writes nothing when an archived path is missing at the commit;
|
||||||
|
no-early-exists SURVIVED
|
||||||
|
no-ancestor killed: ✖ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory;
|
||||||
|
copy-no-canonical killed: ✖ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory;
|
||||||
|
install-no-canonical killed: ✖ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree;✖ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy;
|
||||||
|
install-no-copy-needed killed: ✖ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree;
|
||||||
|
render-no-checkout killed: ✖ install into --dir with --no-reload writes the file whole, then reports unchanged;✖ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree;✖ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends;✖ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged;
|
||||||
|
failed-is-running killed: ✖ install refuses while the unit is running, in any state but inactive or failed, and changes nothing;✖ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not;
|
||||||
|
silent-state-fail killed: ✖ when the state read fails, install and uninstall refuse with one line and change nothing;
|
||||||
|
no-reload-skips-check killed: ✖ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not;✖ when the state read fails, install and uninstall refuse with one line and change nothing;
|
||||||
|
uid-length-range killed: ✖ the uid header takes only an Authentik uid; the username header still takes a username;
|
||||||
|
status-drift-inverted killed: ✖ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy;
|
||||||
|
status-any-copy killed: ✖ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy;
|
||||||
|
queue-root-ignored killed: ✖ consoleReads runs the queue read under queueRoot and reads RELEASE under root;✖ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout;
|
||||||
|
cli-checkout-ignored killed: ✖ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout;
|
||||||
|
checkout-relative-ok killed: ✖ serve refuses a --checkout that is not an absolute directory path;
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (4394.646907ms)
|
||||||
|
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
|
||||||
|
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (5709.932799ms)
|
||||||
|
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (9137.553238ms)
|
||||||
|
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (489.206257ms)
|
||||||
|
✔ decisions route serves the roles and recent lists from the reader view (1167.032106ms)
|
||||||
|
✔ credentials route serves the broker credential status from the reader view (1319.114281ms)
|
||||||
|
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (252.951067ms)
|
||||||
|
✔ bus refusals map to statuses and never carry a path or a stack (76.898274ms)
|
||||||
|
✔ humanCall speaks the human transport protocol without blocking the server (2199.984461ms)
|
||||||
|
✔ busReader takes --business, else the live bus host, else refuses (532.216638ms)
|
||||||
|
✔ humanCall kills a transport that runs past its timeout (531.786336ms)
|
||||||
|
✔ serve refuses a --business value that is not a business id (542.663593ms)
|
||||||
|
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (4489.39161ms)
|
||||||
|
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (4239.827603ms)
|
||||||
|
✔ conversation view: a newer session with no readable history keeps the marker (1957.372203ms)
|
||||||
|
✔ conversation view: seats without history say so and offer no reply (1349.867936ms)
|
||||||
|
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (4537.764374ms)
|
||||||
|
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (56728.881966ms)
|
||||||
|
✔ copy writes the Console paths at the full id, read-only, and leaves an existing copy alone (3549.464574ms)
|
||||||
|
✔ copy refuses a commit off refactor, an unknown one, a worktree and a bad copies directory (3673.626635ms)
|
||||||
|
✔ copy writes nothing when an archived path is missing at the commit (772.072442ms)
|
||||||
|
✔ serve runs from a copy: /healthz 200, the config check, roles and RELEASE from the copy, the queue from --checkout (1636.494489ms)
|
||||||
|
✔ serve refuses a --checkout that is not an absolute directory path (270.968051ms)
|
||||||
|
✔ consoleReads runs the queue read under queueRoot and reads RELEASE under root (63.379735ms)
|
||||||
|
✔ preview options are all three or none, and each is checked (13.216321ms)
|
||||||
|
✔ the CLI refuses a partial or invalid preview flag set at start (3350.904908ms)
|
||||||
|
✔ the CLI with all three flags serves the preview mode (355.802865ms)
|
||||||
|
✔ a request for the public name needs the header equal to the user id, else 401 and nothing read (45.591893ms)
|
||||||
|
✔ the header name matches without regard to case, and the value compare is exact (7.581488ms)
|
||||||
|
✔ the public name with the header reads the bus views, and the Host and Origin are checked first (91.177642ms)
|
||||||
|
✔ in preview mode every write answers 403 read-only, from the public name and loopback, and reaches nothing (30.567202ms)
|
||||||
|
✔ loopback requests are unchanged in preview mode, and the bus views work with the board down (9.673115ms)
|
||||||
|
✔ a proxy that rewrites Host to the loopback port gets 403 on its first request, whatever the user (37.199482ms)
|
||||||
|
✔ a repeated Host is 400 before any other check, in preview mode and without it (16.696137ms)
|
||||||
|
✔ without the preview flags the public name is refused and writes still reach the board (10.184141ms)
|
||||||
|
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.471829ms)
|
||||||
|
✔ credential state from the date alone, as the bus states it (0.288165ms)
|
||||||
|
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (3360.22104ms)
|
||||||
|
✔ queue: a refused read fails closed with the store's text, redacted (1232.944177ms)
|
||||||
|
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (3464.879943ms)
|
||||||
|
✔ business: names, vars on the allowlist, authority, credential metadata only (13.940404ms)
|
||||||
|
✔ business: missing or invalid file refuses with the module's text, redacted (23.645666ms)
|
||||||
|
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (15.000152ms)
|
||||||
|
✔ settings: release, board, loopback address, notifier binding name only (651.12514ms)
|
||||||
|
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (4650.501877ms)
|
||||||
|
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (23113.240556ms)
|
||||||
|
✔ 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 (5971.272011ms)
|
||||||
|
✔ startServer takes send only as a function and only with the preview mode (18.489056ms)
|
||||||
|
✔ the CLI refuses --send without the preview flags, and without a configured bus (2475.985635ms)
|
||||||
|
✔ the CLI with --send and a configured bus sends through the human transport and logs one line (851.240384ms)
|
||||||
|
✔ without --send the route is read-only like every other write, and config says no send (45.110146ms)
|
||||||
|
✔ with --send every other write is still 403 read-only, and nothing reaches the board or bus (28.095422ms)
|
||||||
|
✔ config says send only on the public name with --send on (10.638806ms)
|
||||||
|
✔ a send without the signed-in user is 401 and is neither sent nor logged, loopback included (13.723438ms)
|
||||||
|
✔ each request check refuses before anything is sent, and logs the refusal without the body (27.367598ms)
|
||||||
|
✔ a send passes exactly to, body and in_reply_to, answers the id, and logs it (8.252091ms)
|
||||||
|
✔ a crafted role name cannot forge a log line (13.736446ms)
|
||||||
|
✔ a bus refusal is not sent, with its status; an outcome the bus could not confirm is unknown and never retried (19.06136ms)
|
||||||
|
✔ through the human transport the request is exactly the business, message.send and the keys (358.902005ms)
|
||||||
|
✔ against a real broker: the message is written once as a human send, and its refusals map (624.472798ms)
|
||||||
|
✔ loopback host and board origin fail closed (19.805228ms)
|
||||||
|
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (588.876992ms)
|
||||||
|
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (490.717407ms)
|
||||||
|
✔ unreachable board reports URL; CLI rejects unsupported options (2120.638861ms)
|
||||||
|
✔ render prints the unit without the preview flags (37.951579ms)
|
||||||
|
✔ render carries every serve flag, the preview set included (21.765295ms)
|
||||||
|
✔ a partial preview set is refused (82.659313ms)
|
||||||
|
✔ a value the unit cannot carry is refused, for every flag (683.277932ms)
|
||||||
|
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (73.817595ms)
|
||||||
|
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (270.863433ms)
|
||||||
|
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (197.110792ms)
|
||||||
|
✔ uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not (1029.755501ms)
|
||||||
|
✔ status prints the unit state and GET /healthz on the installed port (404.612111ms)
|
||||||
|
✔ usage: an unknown verb or flag, and start or enable, exit 4 (108.440067ms)
|
||||||
|
✔ without --send, render is the template with its placeholders filled, byte for byte (54.973513ms)
|
||||||
|
✔ render --send with the preview set ends ExecStart with --send and changes nothing else (95.150464ms)
|
||||||
|
✔ --send without the whole preview set is refused and writes nothing (316.03091ms)
|
||||||
|
✔ install --send into --dir with --no-reload writes the sending unit, and status says whether it sends (598.764706ms)
|
||||||
|
✔ uninstall and status take no --send (47.851104ms)
|
||||||
|
✔ install refuses while the unit is running, in any state but inactive or failed, and changes nothing (406.927119ms)
|
||||||
|
✔ when the state read fails, install and uninstall refuse with one line and change nothing (86.166207ms)
|
||||||
|
✔ install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree (91.850506ms)
|
||||||
|
✔ render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged (34.595299ms)
|
||||||
|
✔ the uid header takes only an Authentik uid; the username header still takes a username (838.637048ms)
|
||||||
|
✔ a preview value cannot start with - or . (92.724762ms)
|
||||||
|
✔ status names the copy and whether refactor has moved past it, and says when the unit runs from no copy (476.216748ms)
|
||||||
|
✔ tokens.css and icons.svg are byte copies of docs/design (1.031506ms)
|
||||||
|
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (2.301906ms)
|
||||||
|
✔ shipped strings are unchanged, and the web keeps its write boundary (3.8532ms)
|
||||||
|
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (7911.890688ms)
|
||||||
|
✔ five states on Board, Inbox, Tasks, Agents and Trail (2718.804104ms)
|
||||||
|
✔ Queue, Business and Settings in their five states, with no move button (5732.50335ms)
|
||||||
|
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (2113.350832ms)
|
||||||
|
✔ the server log names no config path when the system config refuses (373.020142ms)
|
||||||
|
ℹ tests 94
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 94
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 57731.276139
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# Row 77, the Console runs from a pinned copy, round 1 review (Darkwing)
|
||||||
|
|
||||||
|
Issue #1571, request comment 27767, packet `agents/rocko/work/queue-77/`
|
||||||
|
(commit `436d5811`). Brief: `docs/plans/2026-10-12_console-pinned-copy.md`
|
||||||
|
§ "Console pinned copy, update step and service hardening". Base
|
||||||
|
`27a1a2bb`. Candidate manifest sha256
|
||||||
|
`4927de6625a0159eb9633d679a348a77a98978cc403bd9eee78fff8c1d6482a0`, 10
|
||||||
|
files, matching the request.
|
||||||
|
|
||||||
|
Verdict: **changes**. The script, the unit and serve do what the brief
|
||||||
|
asks, and N1 to N5 and the uid rule hold under test. Two things stop a
|
||||||
|
copy-run Console from working once it's live, and round 2 is already
|
||||||
|
planned for both. Finding 1 is the one Sage asked me to name: the bus
|
||||||
|
refuses every human proof from a copy. Finding 2 is row 61's new import,
|
||||||
|
which the copy doesn't carry. Finding 3 is a smaller choice about the
|
||||||
|
copy's directories. Notes follow.
|
||||||
|
|
||||||
|
## Method
|
||||||
|
|
||||||
|
- A detached worktree at `27a1a2bb` with `git apply --index build.patch`.
|
||||||
|
`git diff --cached --binary 27a1a2bb` hashes the same as `build.patch`
|
||||||
|
(`e852a913…`). `sha256sum -c` on the manifest: the 8 code files OK in
|
||||||
|
the worktree, the 2 packet files OK in the canonical checkout. No commit
|
||||||
|
from `27a1a2bb` to `70ed77da` touches the 8 code paths.
|
||||||
|
- I read the brief, `BUILD.md`, the whole script, the `cli.mjs`,
|
||||||
|
`reads.mjs` and template diffs, the two doc diffs and both test files.
|
||||||
|
- I walked serve's static import closure from `packages/webui/src/cli.mjs`
|
||||||
|
(`r1/closure2.mjs`, output in `r1/closure-static.txt`) to check
|
||||||
|
`COPY_PATHS` against it.
|
||||||
|
- The webui suite on the candidate, then 19 mutants of my own
|
||||||
|
(`r1/mutants.sh`). Several repeat Rocko's 18 at a different edit; the
|
||||||
|
rest are sites theirs don't name. Each edits one site with
|
||||||
|
`queue-59-review/r1/mut.mjs`, runs one test file and restores the file.
|
||||||
|
- A by-hand check of what a replace-by-rename writer does to a copy
|
||||||
|
(finding 3).
|
||||||
|
|
||||||
|
Node v26.8.1, `TMPDIR=~/darkwing-scratch/rv77/tmp`.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
**1. A Console run from a copy can't prove the human to a bus run from the
|
||||||
|
checkout.** The Console reads the bus by spawning the human transport,
|
||||||
|
`humanCall` in `packages/webui/src/bus.mjs:31`, with
|
||||||
|
`cli = HUMAN_CLI`. `packages/cli/src/transport.mjs:12` resolves that
|
||||||
|
relative to its own file, so from a copy it's
|
||||||
|
`<copy>/packages/bus/src/human-cli.mjs`. The bus runtime hands
|
||||||
|
`verifyHuman` its own `./human-cli.mjs` as `cliPath`
|
||||||
|
(`packages/bus/src/runtime.mjs:68`), and `packages/bus/src/human.mjs:70`
|
||||||
|
refuses unless `p.argv[1] === resolve(cliPath)`. The bus unit runs from
|
||||||
|
the checkout, so every proof from the copy fails with `human-required`.
|
||||||
|
Board, Inbox, Tasks, Agents and Trail all fail; Queue, Business and
|
||||||
|
Settings still read.
|
||||||
|
|
||||||
|
The tests don't reach this. `pinned-copy.test.mjs` serves from a copy with
|
||||||
|
a stand-in socket and checks `/healthz`, the bus name from config and the
|
||||||
|
non-bus reads. No test spawns the copy's human-cli against a bus.
|
||||||
|
|
||||||
|
That's the proof check doing its job, not a defect in it. Round 2, as
|
||||||
|
Sage planned: `install --copy` refuses unless the bus runs from the same
|
||||||
|
copy, with a test for each case. Row 85 (#1579) moves the bus, Console and
|
||||||
|
CLI to one copy, which is where the two meet. Until then the docs need a
|
||||||
|
line saying a copy-run Console needs the bus on the same copy.
|
||||||
|
|
||||||
|
The TOOLS.md text "the roles, the config check and the bus child come from
|
||||||
|
the copy" is true of the files but reads as if that's fine on its own.
|
||||||
|
Round 2 should say the bus child is the human transport and the bus checks
|
||||||
|
its path.
|
||||||
|
|
||||||
|
**2. Row 61 adds an import the copy doesn't carry.** Row 61 (#1550, mine)
|
||||||
|
is landing first. Its `packages/webui/src/reads.mjs:26` imports
|
||||||
|
`loadFields` and `FieldTableError` from `../../tasks/src/fields.mjs`, which
|
||||||
|
reads `packages/tasks/fields.json`. Neither path is in `COPY_PATHS`
|
||||||
|
(`scripts/webui-service.sh:159`), so serve from a copy fails at load once
|
||||||
|
both rows are in. `pinned-copy.test.mjs` builds its fixture from the
|
||||||
|
tree's own packages, so the rebase onto row 61 fails the serve test,
|
||||||
|
which is the guard the brief asked for. Round 2 adds
|
||||||
|
`packages/tasks/src` and `packages/tasks/fields.json`, and the comment
|
||||||
|
above the list names the reason. Sage has passed this to Rocko.
|
||||||
|
|
||||||
|
**3. The copy's files are read-only, but its directories aren't.** The
|
||||||
|
copy takes `a-w` on every file and `0755` on the top directory; the
|
||||||
|
directories under it keep their `git archive` modes, writable by the
|
||||||
|
owner. A writer that replaces by rename still changes a file. In a
|
||||||
|
directory laid out like the copy, `sed -i` rewrote a mode 444 file (exit
|
||||||
|
0, still 444), and a new file could be added beside it. With the
|
||||||
|
directories read-only too, `sed -i` failed ("couldn't open temporary
|
||||||
|
file") and the file stayed as it was.
|
||||||
|
|
||||||
|
The brief says "removes write permission from every file", and the
|
||||||
|
candidate does that, so this is a choice, not a miss. I'd take write
|
||||||
|
permission off the directories as well: the copy is meant to change only
|
||||||
|
by a new `copy`, and git, editors and `cp --remove-destination` all write
|
||||||
|
by rename. If round 2 does it, three things follow. The trap needs
|
||||||
|
`chmod -R u+w` before `rm -rf`, or a failed copy leaves its temp
|
||||||
|
directory. The docs' "copies are kept until someone removes them" needs
|
||||||
|
the removal command, `chmod -R u+w DIR && rm -rf DIR`. A test pins a
|
||||||
|
directory's mode. If Rocko or Sage keep files-only, the README should say
|
||||||
|
the copy guards against a stray write, not a rename.
|
||||||
|
|
||||||
|
## Tests and mutants
|
||||||
|
|
||||||
|
The webui suite on the candidate, 18:55:30Z to 18:56:27Z on 2026-10-12
|
||||||
|
(`r1/webui.txt`): 94 pass, 0 fail, matching Rocko's count. I didn't rerun
|
||||||
|
the scripts suites. Rocko's gate has them green, and Sage reruns it.
|
||||||
|
|
||||||
|
Mutants ran 19:03:22Z to 19:04:27Z (`r1/mutants.txt`), after Sage's and
|
||||||
|
Rocko's runs had finished. `build.patch` hashes the same in the worktree
|
||||||
|
afterwards.
|
||||||
|
|
||||||
|
| Mutant | Edit | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `mv-no-T` | `mv` without `-T` | survives, see below |
|
||||||
|
| `no-file-chmod` | files keep their write bits | killed: the copy test |
|
||||||
|
| `dir-0700` | no `chmod 0755` on the copy's top directory | survives, see below |
|
||||||
|
| `no-trap` | no cleanup trap | killed: the archive failure test |
|
||||||
|
| `no-early-exists` | no `-e` check before the archive | survives, see below |
|
||||||
|
| `no-ancestor` | no `merge-base --is-ancestor` | killed: the copy refusals test |
|
||||||
|
| `copy-no-canonical` | `copy` skips `canonical` | killed: the copy refusals test |
|
||||||
|
| `install-no-canonical` | `install` skips `canonical` | killed: two tests |
|
||||||
|
| `install-no-copy-needed` | `install` runs without `--copy` | killed: the install `--copy` test |
|
||||||
|
| `render-no-checkout` | the unit gets no `--checkout` | killed: four tests |
|
||||||
|
| `failed-is-running` | a failed unit counts as running | killed: two tests |
|
||||||
|
| `silent-state-fail` | a failed state read exits 1 with no message | killed: the state read test |
|
||||||
|
| `no-reload-skips-check` | `uninstall --no-reload` skips the state read | killed: two tests |
|
||||||
|
| `uid-length-range` | the uid rule takes 63 to 65 characters | killed: the uid test |
|
||||||
|
| `status-drift-inverted` | status says the opposite about `refactor` | killed: the status test |
|
||||||
|
| `status-any-copy` | status calls any directory a copy | killed: the status test |
|
||||||
|
| `queue-root-ignored` | `consoleReads` reads the queue under `root` | killed: two tests |
|
||||||
|
| `cli-checkout-ignored` | serve drops `--checkout` | killed: the serve-from-copy test |
|
||||||
|
| `checkout-relative-ok` | serve takes a relative `--checkout` | killed: the refusal test |
|
||||||
|
|
||||||
|
The three survivors:
|
||||||
|
|
||||||
|
- `mv-no-T` and `no-early-exists` behave the same as the candidate in every
|
||||||
|
case a test can set up. Without `-T`, `mv` onto a missing target renames,
|
||||||
|
as `-T` does. Without the early check, the archive is written, `mv -T`
|
||||||
|
fails on the existing copy, the fallback prints `exists:`, and the trap
|
||||||
|
removes the temp directory. Same output, more work. I'd leave both.
|
||||||
|
- `dir-0700` leaves the copy's top directory at `mktemp -d`'s 0700. The unit
|
||||||
|
runs as the same user, so serve still works, and no test checks the
|
||||||
|
mode. If round 2 takes finding 3, its directory mode test kills this
|
||||||
|
one too.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
**N1. `exists:` trusts any directory at the target.** `copy` checks only
|
||||||
|
`[ -e "$target" ]`, and `install` checks only that
|
||||||
|
`packages/webui/src/cli.mjs` is there. A half-removed or hand-edited copy
|
||||||
|
passes both. With read-only directories (finding 3) that's harder to get
|
||||||
|
into; either way, `status` could say whether the copy's files are still
|
||||||
|
read-only. Not for this round.
|
||||||
|
|
||||||
|
**N2. `mv -T` onto an empty directory succeeds.** If the target appears as
|
||||||
|
an empty directory between the `-e` check and the rename, `mv -T` replaces
|
||||||
|
it, and the copy lands. That's a race with nothing in practice, and the
|
||||||
|
result is still a whole copy, so I'd leave it.
|
||||||
|
|
||||||
|
**N3. The closure check.** `COPY_PATHS` covers serve's static imports on
|
||||||
|
this base: webui 4 files, business 10, cli 9, seat 3, conversation 6, bus 3,
|
||||||
|
harness 3 and discord 6. No bare or dynamic imports. `host.mjs` refers to
|
||||||
|
`bus/src/process.mjs` and `store.mjs`, which reach `packages/tasks` and
|
||||||
|
`../schema.sql`, but serve uses only `readHostState` and never runs them.
|
||||||
|
The comment above the list is right for this base; finding 2 changes it.
|
||||||
|
|
||||||
|
## Rocko's question
|
||||||
|
|
||||||
|
Whether the copy's directories should lose write permission too:
|
||||||
|
finding 3. My answer is yes, with the three follow-ons above.
|
||||||
Reference in New Issue
Block a user