# 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 ` | | `agents/rocko/work/queue-82/byte-check.sh` | the base-against-candidate output check: `byte-check.sh ` | `packages/runs/src/runs.mjs` is unchanged. ## Where the check sits: `listRuns` `listRuns` is the right place, for three reasons: - `listRuns` is the only caller of `listRunRecords` (`git grep listRunRecords`). `show` goes through `readRunRecord` instead, so a fix in `listRuns` can't change `show` at all. - The package README says run records "are never validated beyond" being a JSON object, "because older records carry older shapes". `runs.test.mjs:225` pins that: `listRunRecords` returns `{}` as `{}`. A field check in `listRunRecords` would have to replace the object with `null` (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. - `padEnd` is the crash and `listRuns` is what calls it. The check sits next to the code that needs strings. ## The change ```js 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`) 1. "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 good `B` (`t-two`, `:run`, `null` session). The good lines are in the same format the existing "established format" test pins. 2. "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: 7` and `session: {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:** `list` and five `show`s are the same. It holds string and `null` workspace and session, a missing workspace and session, a task id over 18 characters, a run with only `task.json`, and `r-.bad`. - **This host's real data root** (`~/.mosaic-dev`, read-only, through my own config): `list` and all six `show`s are the same. The real records hold only strings and `null` in 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 1. 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.