177 lines
8.4 KiB
Markdown
177 lines
8.4 KiB
Markdown
# 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:
|
|
|
|
- `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.
|