8.4 KiB
Row 82 (#1551): runs list survives a damaged run record, candidate packet, round 2
Author: Rocko. Reviewer: Filbert. Brief:
docs/plans/2026-10-12_runs-list-damaged-record.md, section "Runs list
survives a damaged run record" (blob fb9da977, re-pinned at rev 606;
first briefed as e9c7b769 at rev 601). Base:
876f1ffe (base.txt). The packet is uncommitted. I made no commits,
pushes or Gitea calls and read no token or private binding.
Files (files.txt, candidate-manifest.sha256)
| File | Change |
|---|---|
scripts/mosaic-task.mjs |
listRuns only: each of status, taskId, workspace and session is used only if it's a string |
packages/runs/tests/task-cli.test.mjs |
+2 tests: a {} run between two good ones; one run per field with a value that isn't a string |
packages/runs/README.md |
one row in the damaged-records table, and one clause in the paragraph above it |
agents/rocko/work/queue-82/mutants.sh |
the mutant rerun: mutants.sh <tree> |
agents/rocko/work/queue-82/byte-check.sh |
the base-against-candidate output check: byte-check.sh <base> <candidate> <out> |
packages/runs/src/runs.mjs is unchanged.
Where the check sits: listRuns
listRuns is the right place, for three reasons:
listRunsis the only caller oflistRunRecords(git grep listRunRecords).showgoes throughreadRunRecordinstead, so a fix inlistRunscan't changeshowat all.- The package README says run records "are never validated beyond" being
a JSON object, "because older records carry older shapes".
runs.test.mjs:225pins that:listRunRecordsreturns{}as{}. A field check inlistRunRecordswould have to replace the object withnull(which loses the good fields of a half-damaged record) or rewrite fields. Either change is a contract change to the package, for a problem that only exists in one caller's formatting. padEndis the crash andlistRunsis what calls it. The check sits next to the code that needs strings.
The change
if (result) {
if (typeof result.status === "string") status = result.status;
if (typeof result.taskId === "string") taskId = result.taskId;
if (typeof result.workspace === "string") workspace = result.workspace;
if (typeof result.session === "string") session = result.session;
}
The defaults stay unknown and -. String(workspace) in the print line
became workspace, since workspace is now always a string.
Why a well-formed record prints the same: a run writes workspace and
session from the task, so each is a string or null
(scripts/mosaic-task.mjs:442-444). The base mapped null through
?? "-" to -, and the candidate's default is -. A string passes
through unchanged in both. status and taskId are always strings in a
well-formed record. Anything else crashed the base.
The only output change for a record that didn't crash is a workspace or
session that is neither a string nor null. The base printed it
through String (7, [object Object]) and the candidate prints -.
That's the brief's rule, and the README row says so.
Tests (packages/runs/tests/task-cli.test.mjs)
- "list prints a damaged result.json as unknown and keeps listing the
runs after it". The runs are a good
A(ws1,s1),{}, and a goodB(t-two,:run,nullsession). The good lines are in the same format the existing "established format" test pins. - "list prints each result field that isn't a string as unknown or -".
Four runs, each a good result with one field damaged:
status: 5,taskId: null,workspace: 7andsession: {name: "s1"}.
Between them they cover the brief's {}, numeric status, null
taskId, and a damaged run between two good ones. The second test gives
each of the four checks its own case.
Runs suite: 43 pass on the candidate, against 41 on the base.
Mutants (out/mutants-candidate.txt, out/mutants-base.txt)
Each mutant drops one check and puts that field's base line back:
| Mutant | Line becomes | Base tests | Candidate tests |
|---|---|---|---|
status-unchecked |
status = result.status; |
survived | killed (tests 1 and 2) |
taskid-unchecked |
taskId = result.taskId; |
survived | killed (tests 1 and 2) |
workspace-unchecked |
workspace = String(result.workspace ?? "-"); |
survived | killed (test 2) |
session-unchecked |
session = result.session ?? "-"; |
survived | killed (test 2) |
The base has no checks to drop, so "survives on the base" means this:
the candidate's listRuns with the base's tests. I built that as a
detached worktree at 876f1ffe with only the candidate
scripts/mosaic-task.mjs copied in, and removed it afterwards. The
workspace-unchecked mutant keeps the base's String(). Without it,
7.padEnd would crash, and the mutant would test the crash instead of
the check.
The script saves the file, applies the sed and checks it changed
something. It then runs packages/runs/tests/*.test.mjs and restores the
file. It exits 1 on the base-tests tree, as it should when a mutant
survives.
Byte check (out/byte-check.txt, out/byte-check/)
byte-check.sh runs base and candidate scripts/mosaic-task.mjs on the
same data roots and compares stdout, stderr and exit status:
- Seeded well-formed root:
listand fiveshows are the same. It holds string andnullworkspace and session, a missing workspace and session, a task id over 18 characters, a run with onlytask.json, andr-.bad. - This host's real data root (
~/.mosaic-dev, read-only, through my own config):listand all sixshows are the same. The real records hold only strings andnullin the four fields. - Seeded damaged root, recorded only: the base printed one line and
crashed with
TypeError: Cannot read properties of undefined (reading 'padEnd'), exit 1. The candidate printed all three lines, exit 0.
I removed the real-root output files from out/byte-check/ before
packing, because they carry real runs' requests and responses.
out/byte-check.txt keeps the result line for each.
Round 2: the paragraph above the table
Round 1 (candidate c8cbc227) never went to review. Its packet said the
paragraph above the table claimed list and show print exactly what
they printed before, apart from links, non-object documents and
unreadable directories. That claim became false for a workspace or
session that is neither a string nor null, and the paragraph was
outside the first brief. Sage amended the brief in 09a8e0ee (blob
fb9da977, rev 606). The README entry now owns the "How mosaic-task.mjs
uses it" section, and the paragraph has to name the wrong-field-type case.
Round 2 adds one clause to the conditions: "no result.json with a field
of the wrong type (a status or taskId that isn't a string, or a
workspace or session that is neither a string nor null)". The
wording matches the table row below it. Nothing else changed from round
- Sage ruled that a documentation-only change needs just the runs suite
and byte-check.sh rerun, so I reran both on the round 2 tree
(
out/r2/). Runs suite: 43 pass, 0 fail. byte-check.sh: exit 0, every seeded and real comparison the same, and the damaged list unchanged (base rc 1 and 1 line, candidate rc 0 and 3 lines). As in round 1, I left the real-root output files out of the packet. The gate below is round 1's, on code identical to round 2's.
Gate, round 1 (out/gate/, sequential, scratch worktree at 876f1ffe with the candidate, node_modules linked)
2026-10-12T01:31:06Z to 01:37:34Z. Every suite exited 0. out/gate.sh
is the script, out/gate/summary.txt the summary, and out/gate/*.log
the full logs.
| Suite | Result |
|---|---|
packages/runs |
43 pass, 0 fail |
packages/tasks |
71 pass, 0 fail |
packages/bus |
94 pass, 0 fail |
packages/business |
60 pass, 0 fail |
packages/harness |
59 pass, 0 fail |
packages/cli |
114 pass, 0 fail |
packages/webui |
62 pass, 0 fail |
packages/seat |
27 pass, 0 fail |
test-auth.sh |
15 passed, 0 failed |
test-conductor.sh |
17 passed, 0 failed |
test-config.sh |
24 passed, 0 failed |
test-discord.sh |
66 passed, 0 failed |
test-extension-package.sh |
18 passed, 0 failed |
test-foundation.sh |
44 passed, 0 failed |
test-queue.sh |
27 passed, 0 failed |
test-release.sh |
14 passed, 0 failed |
test-task.sh |
98 passed, 0 failed |
Those are all nine scripts/test-*.sh at the base. Afterwards I removed
the rocko-r82_default network.