feat(runs): read-only runs and releases reader module (#1545, row 56)

New packages/runs reads run records, result.json, the release pointer
and the activation log without writing, pruning or following a link out
of the data root. mosaic-task.mjs list and show use it, so a malformed
result.json or a run that is a regular file no longer crashes them.
Deliberate deltas are listed in the README.

Rocko built it. Round 1 (ed3c5392) was approved with notes by Darkwing
(27115) and Filbert (27116); round 2 (2727198f, tests and wording only)
was approved by Darkwing (27123) and Filbert (27124). Sage's gate on
c9a25a47 plus the candidate: runs 41/0, queue 148/0, webui 22/0,
conversation 182/0 (181/1 in the full run on the K12 cgroup timing
test under load, 182/0 alone), control-board 124/0, every
scripts/test-*.sh 0 failed (task 98/0 with Docker, 26/0 without).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-10 12:22:37 -05:00
co-authored by Claude Opus 5.5
parent 61ec09822f
commit 71d874764a
16 changed files with 1083 additions and 27 deletions
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Row 56 byte-identity check. Runs `mosaic-task.mjs list` and `show` from a
# baseline tree and a candidate tree against the same seeded data roots and
# compares stdout, stderr and exit codes byte for byte. It also checks that
# neither tree changed the data root.
# Usage: byte-check.sh BASE_TREE CAND_TREE WORK_DIR (WORK_DIR must not exist)
set -euo pipefail
BASE="$(cd "$1" && pwd)"; CAND="$(cd "$2" && pwd)"; W="$3"
HERE="$(cd "$(dirname "$0")" && pwd)"
[ ! -e "$W" ] || { echo "byte-check: $W exists" >&2; exit 4; }
mkdir -p "$W/out"; W="$(cd "$W" && pwd)"
"$HERE/seed.sh" "$W/data"
mkdir -p "$W/empty"
conf() { printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m"}}\n' "$2" > "$W/$1.json"; }
conf seeded "$W/data"; conf empty "$W/empty"; conf absent "$W/no-such-root"
snapshot() { (cd "$W/data" && find . -printf '%p %y %s %T@ %l\n' | sort && find . -type f -print0 | sort -z | xargs -0 sha256sum); }
snapshot > "$W/before.txt"
CASES=(
"seeded list"
"empty list"
"absent list"
"seeded show r-20260101T000000Z-aaaaaa"
"seeded show r-20260101T000100Z-bbbbbb"
"seeded show r-20260101T000200Z-cccccc"
"seeded show r-20260101T000300Z-dddddd"
"seeded show r-20260101T000400Z-eeeeee"
"seeded show r-20260101T000500Z-ffffff"
"seeded show r-20260101T000700Z-hhhhhh"
"seeded show r-20260101T000800Z-iiiiii"
"seeded show r-zz.weird_name-1"
"seeded show r-missing"
"seeded show ../escape"
"seeded show"
"absent show r-20260101T000000Z-aaaaaa"
)
for tree in base cand; do
root="$BASE"; [ "$tree" = cand ] && root="$CAND"
i=0
for c in "${CASES[@]}"; do
i=$((i + 1))
read -r cfg op arg <<<"$c"
o="$W/out/$tree/$(printf '%02d' "$i")"
mkdir -p "$o"
printf '%s\n' "$c" > "$o/case"
rc=0
(cd "$root" && env -u NODE_OPTIONS MOSAIC_CONFIG="$W/$cfg.json" node scripts/mosaic-task.mjs "$op" ${arg:+"$arg"} >"$o/stdout" 2>"$o/stderr") || rc=$?
printf '%s\n' "$rc" > "$o/exit"
[ -f "$o/stdout" ] && [ -f "$o/stderr" ] || { echo "byte-check: case $i ($c) left no output in $o" >&2; exit 4; }
done
done
snapshot > "$W/after.txt"
status=0
if diff -r "$W/out/base" "$W/out/cand" > "$W/diff.txt"; then
echo "byte-check: ${#CASES[@]} cases, stdout, stderr and exit identical"
else
echo "byte-check: DIFFERENCES (see $W/diff.txt)"; status=1
fi
if cmp -s "$W/before.txt" "$W/after.txt"; then
echo "byte-check: data root unchanged"
else
echo "byte-check: DATA ROOT CHANGED"; status=1
fi
exit "$status"
@@ -0,0 +1,15 @@
1afe07062349b097b955f0391bffb839b083492d02644421def6c39cc19ff4a4 agents/rocko/work/queue-56/byte-check.sh
09788e902a54207ef5f94de1c8b7f2f31c049fb028fa68847115da83060d90f9 agents/rocko/work/queue-56/delta-check.sh
15d575cc45313906114f7edda9379b66af7624d9e894bb98812deb7acf5d87ba agents/rocko/work/queue-56/seed.sh
146b236a37bf665979e28f1794c4c4437b6e85feb43b892e8d9d697506fc453f packages/runs/package.json
895dd393a43221ffb12a5dc1b76ab61118b5562a5d7acbc50450ebb3ea6388d3 packages/runs/README.md
f686212fa10e8541a1d8055fe30d65558a516245276aa53ecefd97bd7ca0a20c packages/runs/src/errors.mjs
eb7063a8ee0417699b45f4bfec69c3ffcde8b3b276956ca4fea266ec0abd6b23 packages/runs/src/index.mjs
3f9d025f0615a08edd440bb2aa6cea48f21cafa1f680332fffca3c7ee586cc65 packages/runs/src/paths.mjs
ba64e50c3a63e693fc313278ff6cf440234649a6719dd9229bce20272a518ec1 packages/runs/src/runs.mjs
ae546df81c6da7ef51680e23a12c01f08c5a6dc1b68cdf26c8d4210772149940 packages/runs/src/state.mjs
7c941596020b14d8627ccb09b66e0e51195318e86add60eba9d2c4409e6f8dd6 packages/runs/tests/helpers.mjs
fbaa092b5c8a1b54bc52c090da71f0da4313b4a8221fd7a2e89404a2a1b04633 packages/runs/tests/runs.test.mjs
d0f33cb945cb5960847cd9e9af05f5a85731cb68bc5131efb084a7ecea81591f packages/runs/tests/state.test.mjs
be5d1bdf95340e6c4a697531a225f08b40901ad1b570da7371efe7994e3e1413 packages/runs/tests/task-cli.test.mjs
4873187609897c643e62acf7f1d074fa3c03369fe6702b9b0c7776138e776401 scripts/mosaic-task.mjs
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Row 56 deliberate deltas. Outside the seeded data root of byte-check.sh,
# list and show change on purpose where a record links out of the data
# root, a document isn't a JSON object, a run is a regular file, or the
# runs directory can't be read. This prints the baseline and candidate
# output for each such case (first stderr line only) so the change is on
# record. It asserts nothing; the package tests assert the new behaviour.
# Usage: delta-check.sh BASE_TREE CAND_TREE WORK_DIR (WORK_DIR must not exist)
set -euo pipefail
BASE="$(cd "$1" && pwd)"; CAND="$(cd "$2" && pwd)"; W="$3"
[ ! -e "$W" ] || { echo "delta-check: $W exists" >&2; exit 4; }
mkdir -p "$W"; W="$(cd "$W" && pwd)"
A=r-20260101T000000Z-aaaaaa
RESULT='{"runVersion":1,"taskId":"t-out","status":"succeeded","request":"r","response":"s","provider":"p","model":"m","startedAt":"a","finishedAt":"b","durationMs":1,"exitCode":0,"signal":null}'
root() { # name -> creates W/name/{data,outside}, writes config, echoes data root
mkdir -p "$W/$1/data" "$W/$1/outside"
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m"}}\n' "$W/$1/data" > "$W/$1/config.json"
echo "$W/$1/data"
}
run_case() { # name op [arg]
local name="$1"; shift
for tree in base cand; do
local dir="$BASE"; [ "$tree" = cand ] && dir="$CAND"
local rc=0 out err
out="$(cd "$dir" && env MOSAIC_CONFIG="$W/$name/config.json" node scripts/mosaic-task.mjs "$@" 2>"$W/$name/$tree.err")" || rc=$?
err="$(head -1 "$W/$name/$tree.err")"
printf ' %s: exit %s\n' "$tree" "$rc"
[ -z "$out" ] || printf '%s\n' "$out" | sed 's/^/ out| /'
[ -z "$err" ] || printf ' err| %s\n' "$err"
done
}
hdr() { printf '\n== %s\n' "$*"; }
D="$(root run-link)"; mkdir -p "$D/runs"; printf '%s\n' "$RESULT" > "$W/run-link/outside/result.json"; ln -s "$W/run-link/outside" "$D/runs/$A"
hdr "run directory linked out of the data root: list"; run_case run-link list
hdr "run directory linked out of the data root: show"; run_case run-link show "$A"
D="$(root doc-link)"; mkdir -p "$D/runs/$A"; printf '%s\n' "$RESULT" > "$W/doc-link/outside/result.json"; ln -s "$W/doc-link/outside/result.json" "$D/runs/$A/result.json"
hdr "result.json linked out of the data root: list"; run_case doc-link list
hdr "result.json linked out of the data root: show"; run_case doc-link show "$A"
D="$(root runs-link)"; mkdir -p "$W/runs-link/outside/$A"; printf '%s\n' "$RESULT" > "$W/runs-link/outside/$A/result.json"; ln -s "$W/runs-link/outside" "$D/runs"
hdr "runs directory linked out of the data root: list"; run_case runs-link list
hdr "runs directory linked out of the data root: show"; run_case runs-link show "$A"
D="$(root non-object)"; mkdir -p "$D/runs/$A"; printf '5\n' > "$D/runs/$A/result.json"; printf '[]\n' > "$D/runs/$A/task.json"
hdr "result.json is 5 and task.json is []: list"; run_case non-object list
hdr "result.json is 5 and task.json is []: show"; run_case non-object show "$A"
D="$(root run-file)"; mkdir -p "$D/runs"; printf 'x\n' > "$D/runs/$A"
hdr "run is a regular file: show"; run_case run-file show "$A"
D="$(root loop)"; mkdir -p "$D/runs"; ln -s r-b "$D/runs/$A"; ln -s "$A" "$D/runs/r-b"
hdr "run is a link loop: show"; run_case loop show "$A"
if [ "$(id -u)" != 0 ]; then
D="$(root unreadable)"; mkdir -p "$D/runs/$A"; chmod 000 "$D/runs"
hdr "runs directory unreadable: list"; run_case unreadable list
hdr "runs directory unreadable: show"; run_case unreadable show "$A"
chmod 755 "$D/runs"
D="$(root run-unreadable)"; mkdir -p "$D/runs/$A"; printf '%s\n' "$RESULT" > "$D/runs/$A/result.json"; chmod 000 "$D/runs/$A"
hdr "run directory unreadable: show"; run_case run-unreadable show "$A"
chmod 755 "$D/runs/$A"
fi
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Seeds a data root for the row 56 byte-identity check. Usage: seed.sh DIR
# DIR must not exist. Writes run records only under DIR.
set -euo pipefail
D="$1"
[ ! -e "$D" ] || { echo "seed: $D exists" >&2; exit 4; }
R="$D/runs"
mkdir -p "$R" "$D/state"
run() { mkdir -p "$R/$1"; }
put() { printf '%s\n' "$3" > "$R/$1/$2"; }
A=r-20260101T000000Z-aaaaaa
run $A
put $A task.json '{"taskVersion":1,"id":"t-full","prompt":"say hi","mission":"m.json"}'
put $A mission.json '{"missionVersion":1,"id":"m-full","objective":"Exercise every show line"}'
put $A result.json '{"runVersion":1,"runId":"'$A'","taskId":"t-full","missionId":"m-full","status":"succeeded","reason":null,"request":"say \"hi\"\nplease","response":"hi","expectedExact":"hi","retriedFrom":"r-20251231T000000Z-000000","workspace":"ws1","tools":["read","ls"],"session":"s1","sessionForkFrom":null,"exitCode":0,"signal":null,"provider":"zai","model":"m","startedAt":"2026-01-01T00:00:00.000Z","finishedAt":"2026-01-01T00:00:01.000Z","durationMs":1000}'
: > "$R/$A/stderr.txt"
mkdir "$R/$A/workspace"
B=r-20260101T000100Z-bbbbbb
run $B
put $B task.json '{"taskVersion":1,"id":"t-fail","prompt":"x"}'
put $B result.json '{"runVersion":1,"runId":"'$B'","taskId":"t-fail","missionId":null,"status":"failed","reason":"expect-mismatch","request":"x","response":"y","expectedExact":null,"workspace":null,"tools":null,"session":null,"sessionForkFrom":null,"exitCode":null,"signal":"SIGKILL","provider":"zai","model":"m","startedAt":"2026-01-01T00:01:00.000Z","finishedAt":"2026-01-01T00:01:02.000Z","durationMs":2000}'
C=r-20260101T000200Z-cccccc
run $C
put $C task.json '{"taskVersion":1,"id":"t-incomplete","prompt":"x"}'
E=r-20260101T000300Z-dddddd
run $E
put $E result.json '{"status": "succeeded", truncated'
N=r-20260101T000400Z-eeeeee
run $N
put $N result.json 'null'
F=r-20260101T000500Z-ffffff
run $F
put $F mission.json '{not json'
put $F result.json '{"runVersion":1,"runId":"'$F'","taskId":"a-task-id-longer-than-eighteen","missionId":"m","status":"succeeded-with-a-long-status","reason":null,"request":"","response":"","workspace":":run","tools":[],"session":"named-session","exitCode":0,"signal":null,"provider":"p","model":"m","startedAt":"s","finishedAt":"f","durationMs":0}'
printf 'not a run directory\n' > "$R/r-20260101T000600Z-gggggg"
ln -s "$A" "$R/r-20260101T000700Z-hhhhhh"
ln -s "r-does-not-exist" "$R/r-20260101T000800Z-iiiiii"
run r-zz.weird_name-1
mkdir "$R/x-not-a-run"
printf '{"at":"2026-01-01T00:00:00Z","event":"pruned","runId":"r-old"}\n' > "$R/.pruned.log"
printf 'notes\n' > "$R/notes.txt"
+92
View File
@@ -0,0 +1,92 @@
# Runs
Read-only readers for the run records under `<dataRoot>/runs/` and the
release state under `<dataRoot>/state/` (#1545). `scripts/mosaic-task.mjs
list` and `show` read through it, and so will the console's runs view.
This README covers what the code does and the limits it accepts.
```sh
node --test packages/runs/tests/
```
## What it reads
| Export | Returns |
|---|---|
| `listRunIds(dataRoot)` | every name under `runs/` that starts with `r-`, sorted, which is oldest first |
| `listRunRecords(dataRoot)` | `[{ runId, result }]` for those names; `result` is `null` for an incomplete or unreadable record |
| `readRunRecord(dataRoot, runId)` | `{ runId, result, task, mission, artifacts }`, or `null` when the run doesn't exist or isn't a directory |
| `readRunDocument(dataRoot, runId, name)` | `result.json`, `task.json` or `mission.json` from one run, or `null` |
| `readActivePointer(dataRoot)` | `state/active.json`, or `null` when no release has been activated |
| `readActivationLog(dataRoot, { last })` | `{ entries, malformed }` from `state/activation-log.jsonl`, oldest first |
| `isRunId(value)`, `RUN_ID_PATTERN` | the run id shape `mosaic-task.mjs` checks: `r-` then 1 to 64 of `[A-Za-z0-9._-]`, starting with a letter or digit |
The caller passes `dataRoot`; this package doesn't read the system
config. `artifacts` are names in directory order, as `show` has always
printed them.
## Limits
- **Nothing writes.** No reader creates, changes, prunes or locks
anything. Run records stay write-once evidence; pruning stays in
`mosaic-task.mjs prune`.
- **Nothing follows a link out of the data root.** The configured data
root is trusted as given, even when it is itself a link. Every path
below it is resolved, and one that resolves outside it is refused or
read as unreadable:
- `runs/` or `state/` resolving outside refuses with a `RunsError`;
- a run directory resolving outside: `readRunRecord` refuses, and
`listRunRecords` gives that run a `null` result;
- a run document resolving outside reads as `null`;
- `active.json` or `activation-log.jsonl` resolving outside refuses.
Only a path that exists can resolve. A link with nothing behind it reads
as missing wherever it points. A `state/` link out of the data root to a
path that doesn't exist reads as no release and an empty log, and a
`runs/` link like that lists nothing. A link that stays inside the data
root is followed.
- **Run documents are JSON objects or `null`.** A document that is
missing, unreadable, not JSON, or JSON but not an object (`null`, `5`,
`[]`) reads as `null`. Run records are never validated beyond that,
because older records carry older shapes.
- **The release pointer is strict.** `active.json` must be the version 1
shape `scripts/release.sh` writes: exactly `pointerVersion` 1 and
non-empty strings `release`, `imageTag` and `activatedAt`. Anything else
refuses with exit code 2 rather than being guessed at.
- **The activation log is read per line.** One bad line never refuses
the log. An entry needs string `at`, `event`, `release` and `imageTag`,
an optional string `note`, and no other keys. A line that isn't JSON,
or is JSON with another shape, is counted in `malformed` and skipped.
That is stricter than `release.sh rollback`, which skips only lines that
aren't JSON and would act on an entry with an extra key or a non-string
field. Blank lines aren't counted. `last` must be a positive integer and
keeps the newest entries.
- **Errors.** Every refusal is a `RunsError` with `exitCode` 2 (invalid
data) or 4 (a file or environment problem), matching
`mosaic-task.mjs`. A missing data root, `runs/` or `state/` file is not
an error. A path that can't be resolved for another reason (a link
loop, a permission error) or a directory that can't be read refuses
instead of reading as empty.
## How mosaic-task.mjs uses it
`list` and `show` print exactly what they printed before this package
existed, for any data root with no link out of it, no run document that
is JSON but not an object, and no unreadable directory.
`agents/rocko/work/queue-56/byte-check.sh` checks that byte for byte
against a seeded data root. Where those conditions don't hold, the
output changes on purpose:
| Case | Before | Now |
|---|---|---|
| run directory or `result.json` linked out | read through the link | `list`: `unknown`; `show`: refuses (run directory) or `result.json: (missing or unreadable)` |
| `runs/` linked out | read through the link | `list` and `show` refuse, exit 4 |
| `result.json` is `5` or `[]` | `list` crashed; `show` printed `undefined` fields | `unknown`; `(missing or unreadable)` |
| `task.json` or `mission.json` is JSON but not an object | `show` printed its snapshot line | snapshot line omitted |
| run is a regular file | `show` crashed | `run not found`, exit 4 |
| `runs/` unreadable | `list` printed nothing | refuses, exit 4 |
| run directory unreadable (mode 000) | `show` printed two lines, then crashed, exit 1 | refuses with EACCES, exit 4 |
| link loop or a permission error resolving a run | `run not found` | refuses with the error code, exit 4 |
`agents/rocko/work/queue-56/delta-check.sh` prints the before and after
for each case.
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@mosaic/runs",
"version": "0.1.0",
"private": true,
"description": "Read-only readers for run records under <dataRoot>/runs/ and the release pointer and activation log under <dataRoot>/state/.",
"license": "UNLICENSED",
"type": "module",
"engines": { "node": ">=24" },
"exports": { ".": "./src/index.mjs" },
"scripts": { "test": "node --test tests/" }
}
+9
View File
@@ -0,0 +1,9 @@
// Exit codes follow scripts/mosaic-task.mjs: 2 invalid data, 4 a file or
// environment problem. Every refusal in this package is a RunsError.
export class RunsError extends Error {
constructor(message, exitCode = 4) {
super(message);
this.name = "RunsError";
this.exitCode = exitCode;
}
}
+10
View File
@@ -0,0 +1,10 @@
// @mosaic/runs: read-only readers for run records under <dataRoot>/runs/
// and the release pointer and activation log under <dataRoot>/state/.
// Nothing here writes, prunes or follows a link out of the data root.
export { RunsError } from "./errors.mjs";
export { RUNS_DIRNAME, STATE_DIRNAME } from "./paths.mjs";
export {
RUN_ID_PATTERN, RUN_DOCUMENTS, isRunId, listRunIds, readRunDocument, listRunRecords, readRunRecord,
} from "./runs.mjs";
export { POINTER_FILE, ACTIVATION_LOG_FILE, readActivePointer, readActivationLog } from "./state.mjs";
+49
View File
@@ -0,0 +1,49 @@
import fs from "node:fs";
import path from "node:path";
import { RunsError } from "./errors.mjs";
export const RUNS_DIRNAME = "runs";
export const STATE_DIRNAME = "state";
export function isMissing(error) {
return error?.code === "ENOENT" || error?.code === "ENOTDIR";
}
function isInside(root, target) {
const relative = path.relative(root, target);
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
}
// Resolves <dataRoot>/<parts...> through any symbolic links and returns the
// real path, or null when it doesn't exist. The data root is trusted as
// configured; a path below it that resolves outside it refuses, so no reader
// here follows a link out of the data root.
export function resolveInside(dataRoot, ...parts) {
const target = path.join(dataRoot, ...parts);
let root;
let real;
try {
root = fs.realpathSync(dataRoot);
real = fs.realpathSync(target);
} catch (error) {
if (isMissing(error)) return null;
throw new RunsError(`cannot resolve ${target}: ${error.code ?? error.message}`);
}
if (!isInside(root, real)) {
throw new RunsError(`${target} resolves outside the data root (${root})`);
}
return real;
}
// A JSON object read from <dataRoot>/<parts...>, or null when the file is
// missing, unreadable, not JSON, not an object or outside the data root.
export function readJsonObject(dataRoot, ...parts) {
try {
const file = resolveInside(dataRoot, ...parts);
if (file === null) return null;
const value = JSON.parse(fs.readFileSync(file, "utf8"));
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
} catch {
return null;
}
}
+72
View File
@@ -0,0 +1,72 @@
import fs from "node:fs";
import { RunsError } from "./errors.mjs";
import { RUNS_DIRNAME, isMissing, readJsonObject, resolveInside } from "./paths.mjs";
export const RUN_ID_PATTERN = /^r-[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
export const RUN_DOCUMENTS = ["result.json", "task.json", "mission.json"];
export function isRunId(value) {
return typeof value === "string" && RUN_ID_PATTERN.test(value);
}
function requireRunId(runId) {
if (!isRunId(runId)) throw new RunsError(`invalid run id: ${JSON.stringify(runId)} (expected r-<id>)`);
}
// Every name under <dataRoot>/runs/ that starts with "r-", sorted. Run ids
// begin with a UTC stamp, so the order is oldest first. A missing data root
// or runs directory is an empty list.
export function listRunIds(dataRoot) {
const root = resolveInside(dataRoot, RUNS_DIRNAME);
if (root === null) return [];
let names;
try {
names = fs.readdirSync(root);
} catch (error) {
if (isMissing(error)) return [];
throw new RunsError(`cannot read ${root}: ${error.code ?? error.message}`);
}
return names.filter((name) => name.startsWith("r-")).sort();
}
// One of RUN_DOCUMENTS from a run, or null when it is missing, unreadable,
// not a JSON object or outside the data root.
export function readRunDocument(dataRoot, runId, name) {
requireRunId(runId);
if (!RUN_DOCUMENTS.includes(name)) throw new RunsError(`unknown run document: ${JSON.stringify(name)}`);
return readJsonObject(dataRoot, RUNS_DIRNAME, runId, name);
}
// [{ runId, result }] for every listed run; result is null for an
// incomplete or unreadable record. Names come from listRunIds, so a name
// that starts with "r-" but isn't a valid run id is still listed.
export function listRunRecords(dataRoot) {
return listRunIds(dataRoot).map((runId) => ({
runId,
result: readJsonObject(dataRoot, RUNS_DIRNAME, runId, "result.json"),
}));
}
// One run's documents and artifact names, or null when the run directory
// doesn't exist or isn't a directory. Artifacts are in directory order.
export function readRunRecord(dataRoot, runId) {
requireRunId(runId);
// The runs directory first, so a refusal names the link that escapes.
if (resolveInside(dataRoot, RUNS_DIRNAME) === null) return null;
const dir = resolveInside(dataRoot, RUNS_DIRNAME, runId);
if (dir === null) return null;
let artifacts;
try {
artifacts = fs.readdirSync(dir);
} catch (error) {
if (isMissing(error)) return null;
throw new RunsError(`cannot read ${dir}: ${error.code ?? error.message}`);
}
return {
runId,
result: readRunDocument(dataRoot, runId, "result.json"),
task: readRunDocument(dataRoot, runId, "task.json"),
mission: readRunDocument(dataRoot, runId, "mission.json"),
artifacts,
};
}
+83
View File
@@ -0,0 +1,83 @@
import fs from "node:fs";
import { RunsError } from "./errors.mjs";
import { STATE_DIRNAME, resolveInside } from "./paths.mjs";
export const POINTER_FILE = "active.json";
export const ACTIVATION_LOG_FILE = "activation-log.jsonl";
const POINTER_KEYS = ["pointerVersion", "release", "imageTag", "activatedAt"];
const LOG_KEYS = ["at", "event", "release", "imageTag", "note"];
function isNonEmptyString(value) {
return typeof value === "string" && value.length > 0;
}
function readStateFile(dataRoot, name) {
const file = resolveInside(dataRoot, STATE_DIRNAME, name);
if (file === null) return null;
try {
return { file, text: fs.readFileSync(file, "utf8") };
} catch (error) {
throw new RunsError(`cannot read ${file}: ${error.code ?? error.message}`);
}
}
// The active release pointer that scripts/release.sh writes, or null when
// no release has been activated. A pointer that isn't the version 1 shape
// refuses rather than being guessed at.
export function readActivePointer(dataRoot) {
const state = readStateFile(dataRoot, POINTER_FILE);
if (state === null) return null;
let pointer;
try {
pointer = JSON.parse(state.text);
} catch (error) {
throw new RunsError(`release pointer is not valid JSON (${state.file}): ${error.message}`, 2);
}
const invalid = (why) => new RunsError(`release pointer ${why} (${state.file})`, 2);
if (typeof pointer !== "object" || pointer === null || Array.isArray(pointer)) throw invalid("must be a JSON object");
for (const key of Object.keys(pointer)) {
if (!POINTER_KEYS.includes(key)) throw invalid(`has an unsupported key: "${key}"`);
}
if (pointer.pointerVersion !== 1) throw invalid(`has unsupported pointerVersion ${JSON.stringify(pointer.pointerVersion)}`);
for (const key of ["release", "imageTag", "activatedAt"]) {
if (!isNonEmptyString(pointer[key])) throw invalid(`needs a non-empty string "${key}"`);
}
return { pointerVersion: 1, release: pointer.release, imageTag: pointer.imageTag, activatedAt: pointer.activatedAt };
}
function logEntry(line) {
let entry;
try {
entry = JSON.parse(line);
} catch {
return null;
}
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return null;
if (Object.keys(entry).some((key) => !LOG_KEYS.includes(key))) return null;
if (!["at", "event", "release", "imageTag"].every((key) => typeof entry[key] === "string")) return null;
if (entry.note !== undefined && typeof entry.note !== "string") return null;
return entry;
}
// The activation log as { entries, malformed }, oldest first. A line that
// isn't JSON, or is JSON but not the entry shape release.sh writes, is
// counted in malformed and skipped. This is stricter than release.sh
// rollback, which skips only lines that aren't JSON. A missing log is empty.
// With last, only the newest last well-formed entries are returned.
export function readActivationLog(dataRoot, { last } = {}) {
if (last !== undefined && (!Number.isInteger(last) || last < 1)) {
throw new RunsError(`last must be a positive integer (got ${JSON.stringify(last)})`);
}
const state = readStateFile(dataRoot, ACTIVATION_LOG_FILE);
if (state === null) return { entries: [], malformed: 0 };
const entries = [];
let malformed = 0;
for (const line of state.text.split("\n")) {
if (line.trim() === "") continue;
const entry = logEntry(line);
if (entry === null) malformed += 1;
else entries.push(entry);
}
return { entries: last === undefined ? entries : entries.slice(-last), malformed };
}
+60
View File
@@ -0,0 +1,60 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// A fresh scratch directory for one test, removed after it. Returns
// { dir, dataRoot, outside }: the data root and a sibling outside it.
export function scratch(t) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mosaic-runs-test-"));
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
const dataRoot = path.join(dir, "data");
const outside = path.join(dir, "outside");
fs.mkdirSync(dataRoot);
fs.mkdirSync(outside);
return { dir, dataRoot, outside };
}
export function write(file, content) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, typeof content === "string" ? content : `${JSON.stringify(content)}\n`);
}
// A listing of every path under dir with its type, size, mtime and link
// target, to show that a reader changed nothing.
export function tree(dir) {
const lines = [];
const walk = (current) => {
for (const name of fs.readdirSync(current).sort()) {
const file = path.join(current, name);
const stat = fs.lstatSync(file);
const link = stat.isSymbolicLink() ? fs.readlinkSync(file) : "";
lines.push(`${path.relative(dir, file)} ${stat.mode} ${stat.size} ${stat.mtimeMs} ${link}`);
if (stat.isDirectory()) walk(file);
}
};
walk(dir);
return lines.join("\n");
}
export const RESULT = {
runVersion: 1,
runId: "r-20260101T000000Z-aaaaaa",
taskId: "t-one",
missionId: null,
status: "succeeded",
reason: null,
request: "hi",
response: "hi",
expectedExact: null,
workspace: null,
tools: null,
session: null,
sessionForkFrom: null,
exitCode: 0,
signal: null,
provider: "zai",
model: "m",
startedAt: "2026-01-01T00:00:00.000Z",
finishedAt: "2026-01-01T00:00:01.000Z",
durationMs: 1000,
};
+243
View File
@@ -0,0 +1,243 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import {
RunsError, isRunId, listRunIds, listRunRecords, readRunDocument, readRunRecord,
} from "../src/index.mjs";
import { RESULT, scratch, tree, write } from "./helpers.mjs";
const A = "r-20260101T000000Z-aaaaaa";
const B = "r-20260101T000100Z-bbbbbb";
test("isRunId accepts the run id shape and nothing else", () => {
for (const id of [A, "r-x", "r-zz.weird_name-1", `r-${"a".repeat(64)}`]) assert.equal(isRunId(id), true, id);
for (const id of ["r-", "r-.x", "r-_x", `r-${"a".repeat(65)}`, "x-1", "r-a/b", "../r-a", "r-a b", 1, null, undefined]) {
assert.equal(isRunId(id), false, String(id));
}
});
test("a missing data root or runs directory lists nothing", (t) => {
const { dir, dataRoot } = scratch(t);
assert.deepEqual(listRunIds(path.join(dir, "absent")), []);
assert.deepEqual(listRunIds(dataRoot), []);
assert.deepEqual(listRunRecords(dataRoot), []);
});
test("runs as a regular file lists nothing, as before", (t) => {
const { dataRoot } = scratch(t);
write(path.join(dataRoot, "runs"), "not a directory\n");
assert.deepEqual(listRunIds(dataRoot), []);
});
test("listRunIds keeps r- names only, sorted oldest first", (t) => {
const { dataRoot } = scratch(t);
const runs = path.join(dataRoot, "runs");
for (const name of [B, A, "x-other", "r-zz"]) fs.mkdirSync(path.join(runs, name), { recursive: true });
write(path.join(runs, ".pruned.log"), "{}\n");
write(path.join(runs, "r-a-file"), "x\n");
assert.deepEqual(listRunIds(dataRoot), [A, B, "r-a-file", "r-zz"]);
});
test("listRunRecords returns each result, or null for an incomplete or unreadable one", (t) => {
const { dataRoot } = scratch(t);
const runs = path.join(dataRoot, "runs");
write(path.join(runs, A, "result.json"), RESULT);
write(path.join(runs, B, "task.json"), { id: "t" });
write(path.join(runs, "r-c", "result.json"), "{ truncated");
write(path.join(runs, "r-d", "result.json"), "null\n");
write(path.join(runs, "r-e", "result.json"), "5\n");
write(path.join(runs, "r-f", "result.json"), "[1]\n");
fs.mkdirSync(path.join(runs, "r-g", "result.json"), { recursive: true });
write(path.join(runs, "r-h"), "a file\n");
const records = listRunRecords(dataRoot);
assert.deepEqual(records.map((r) => r.runId), [A, B, "r-c", "r-d", "r-e", "r-f", "r-g", "r-h"]);
assert.deepEqual(records[0].result, RESULT);
for (const record of records.slice(1)) assert.equal(record.result, null, record.runId);
});
test("readRunRecord returns documents and artifacts in directory order", (t) => {
const { dataRoot } = scratch(t);
const dir = path.join(dataRoot, "runs", A);
write(path.join(dir, "result.json"), RESULT);
write(path.join(dir, "task.json"), { taskVersion: 1, id: "t-one" });
write(path.join(dir, "mission.json"), { id: "m", objective: "o" });
write(path.join(dir, "stderr.txt"), "");
fs.mkdirSync(path.join(dir, "workspace"));
const record = readRunRecord(dataRoot, A);
assert.equal(record.runId, A);
assert.deepEqual(record.result, RESULT);
assert.deepEqual(record.task, { taskVersion: 1, id: "t-one" });
assert.deepEqual(record.mission, { id: "m", objective: "o" });
assert.deepEqual(record.artifacts, fs.readdirSync(dir));
});
test("readRunRecord is null for a missing run, a dangling link or a file", (t) => {
const { dataRoot } = scratch(t);
const runs = path.join(dataRoot, "runs");
assert.equal(readRunRecord(dataRoot, A), null);
fs.mkdirSync(runs);
fs.symlinkSync("r-nowhere", path.join(runs, A));
write(path.join(runs, B), "a file\n");
assert.equal(readRunRecord(dataRoot, A), null);
assert.equal(readRunRecord(dataRoot, B), null);
});
test("readRunRecord and readRunDocument refuse an invalid run id before touching the disk", (t) => {
const { dataRoot } = scratch(t);
for (const id of ["../data", "r-a/../../x", "", "r-"]) {
assert.throws(() => readRunRecord(dataRoot, id), (e) => e instanceof RunsError && e.exitCode === 4 && /invalid run id/.test(e.message));
assert.throws(() => readRunDocument(dataRoot, id, "result.json"), RunsError);
}
});
test("readRunDocument reads only the three run documents", (t) => {
const { dataRoot } = scratch(t);
write(path.join(dataRoot, "runs", A, "stderr.txt"), "{}\n");
assert.throws(() => readRunDocument(dataRoot, A, "stderr.txt"), /unknown run document/);
assert.throws(() => readRunDocument(dataRoot, A, "../../x.json"), /unknown run document/);
assert.equal(readRunDocument(dataRoot, A, "task.json"), null);
});
test("a link inside the data root is followed", (t) => {
const { dataRoot } = scratch(t);
const runs = path.join(dataRoot, "runs");
write(path.join(runs, A, "result.json"), RESULT);
fs.symlinkSync(A, path.join(runs, B));
assert.deepEqual(readRunRecord(dataRoot, B).result, RESULT);
assert.deepEqual(listRunRecords(dataRoot)[1], { runId: B, result: RESULT });
});
test("a data root that is itself a link is trusted as configured", (t) => {
const { dir, dataRoot } = scratch(t);
write(path.join(dataRoot, "runs", A, "result.json"), RESULT);
const alias = path.join(dir, "alias");
fs.symlinkSync(dataRoot, alias);
assert.deepEqual(listRunRecords(alias), [{ runId: A, result: RESULT }]);
});
test("a run directory linked out of the data root is never read", (t) => {
const { dataRoot, outside } = scratch(t);
write(path.join(outside, "result.json"), RESULT);
write(path.join(outside, "task.json"), { id: "t" });
fs.mkdirSync(path.join(dataRoot, "runs"));
fs.symlinkSync(outside, path.join(dataRoot, "runs", A));
assert.deepEqual(listRunRecords(dataRoot), [{ runId: A, result: null }]);
assert.throws(() => readRunRecord(dataRoot, A), (e) => e instanceof RunsError && e.exitCode === 4 && /resolves outside the data root/.test(e.message));
});
test("a document linked out of the data root reads as null", (t) => {
const { dataRoot, outside } = scratch(t);
write(path.join(outside, "secret.json"), RESULT);
fs.mkdirSync(path.join(dataRoot, "runs", A), { recursive: true });
for (const name of ["result.json", "task.json", "mission.json"]) {
fs.symlinkSync(path.join(outside, "secret.json"), path.join(dataRoot, "runs", A, name));
}
const record = readRunRecord(dataRoot, A);
assert.equal(record.result, null);
assert.equal(record.task, null);
assert.equal(record.mission, null);
assert.deepEqual(listRunRecords(dataRoot), [{ runId: A, result: null }]);
});
test("a runs directory linked out of the data root refuses", (t) => {
const { dataRoot, outside } = scratch(t);
write(path.join(outside, A, "result.json"), RESULT);
fs.symlinkSync(outside, path.join(dataRoot, "runs"));
assert.throws(() => listRunIds(dataRoot), /runs resolves outside the data root/);
assert.throws(() => listRunRecords(dataRoot), RunsError);
assert.throws(() => readRunRecord(dataRoot, A), /runs resolves outside the data root/);
});
test("a relative link that climbs out of the data root refuses", (t) => {
const { dataRoot } = scratch(t);
fs.mkdirSync(path.join(dataRoot, "runs"));
fs.symlinkSync("../../outside", path.join(dataRoot, "runs", A));
assert.throws(() => readRunRecord(dataRoot, A), /resolves outside the data root/);
});
test("a sibling whose name starts with the data root's name is outside it", (t) => {
const { dir, dataRoot } = scratch(t);
const sibling = path.join(dir, "data-sibling");
write(path.join(sibling, "result.json"), RESULT);
fs.mkdirSync(path.join(dataRoot, "runs"));
fs.symlinkSync(sibling, path.join(dataRoot, "runs", A));
assert.throws(() => readRunRecord(dataRoot, A), /resolves outside the data root/);
});
test("a link loop refuses instead of reading as missing", (t) => {
const { dataRoot } = scratch(t);
fs.mkdirSync(path.join(dataRoot, "runs"));
fs.symlinkSync(B, path.join(dataRoot, "runs", A));
fs.symlinkSync(A, path.join(dataRoot, "runs", B));
assert.throws(() => readRunRecord(dataRoot, A), (e) => e instanceof RunsError && /ELOOP/.test(e.message));
assert.deepEqual(listRunRecords(dataRoot), [{ runId: A, result: null }, { runId: B, result: null }]);
});
test("an unreadable runs directory refuses instead of listing nothing", { skip: process.getuid?.() === 0 && "root reads anything" }, (t) => {
const { dataRoot } = scratch(t);
const runs = path.join(dataRoot, "runs");
fs.mkdirSync(path.join(runs, A), { recursive: true });
fs.chmodSync(runs, 0o000);
try {
assert.throws(() => listRunIds(dataRoot), (e) => e instanceof RunsError && /EACCES/.test(e.message));
} finally {
fs.chmodSync(runs, 0o755);
}
});
test("an unreadable run directory refuses instead of reading as missing", { skip: process.getuid?.() === 0 && "root reads anything" }, (t) => {
const { dataRoot } = scratch(t);
const run = path.join(dataRoot, "runs", A);
write(path.join(run, "result.json"), RESULT);
fs.chmodSync(run, 0o000);
try {
assert.throws(() => readRunRecord(dataRoot, A), (e) => e instanceof RunsError && /EACCES/.test(e.message));
} finally {
fs.chmodSync(run, 0o755);
}
});
test("a link to the data root's parent is outside it", (t) => {
const { dataRoot } = scratch(t);
fs.mkdirSync(path.join(dataRoot, "runs"));
fs.symlinkSync("../..", path.join(dataRoot, "runs", A));
assert.throws(() => readRunRecord(dataRoot, A), /resolves outside the data root/);
});
// readdir order is the filesystem's; node may already return it sorted, so
// the directory listing is mocked to come back reversed.
test("listRunIds sorts whatever order the directory returns", (t) => {
const { dataRoot } = scratch(t);
for (const id of [A, B]) fs.mkdirSync(path.join(dataRoot, "runs", id), { recursive: true });
const readdirSync = fs.readdirSync;
t.mock.method(fs, "readdirSync", (...args) => readdirSync(...args).sort().reverse());
assert.deepEqual(fs.readdirSync(path.join(dataRoot, "runs")), [B, A]);
assert.deepEqual(listRunIds(dataRoot), [A, B]);
});
// list has always shown any r- name, including ones show refuses as ids.
test("listRunRecords lists an r- name that isn't a valid run id, as before", (t) => {
const { dataRoot } = scratch(t);
write(path.join(dataRoot, "runs", "r-.bad", "result.json"), {});
assert.equal(isRunId("r-.bad"), false);
assert.deepEqual(listRunIds(dataRoot), ["r-.bad"]);
assert.deepEqual(listRunRecords(dataRoot), [{ runId: "r-.bad", result: {} }]);
});
test("the readers write nothing", (t) => {
const { dataRoot } = scratch(t);
const runs = path.join(dataRoot, "runs");
write(path.join(runs, A, "result.json"), RESULT);
write(path.join(runs, A, "task.json"), { id: "t" });
write(path.join(runs, B, "result.json"), "{ bad");
fs.symlinkSync(A, path.join(runs, "r-link"));
const before = tree(dataRoot);
listRunIds(dataRoot);
listRunRecords(dataRoot);
readRunRecord(dataRoot, A);
readRunRecord(dataRoot, B);
readRunRecord(dataRoot, "r-absent");
readRunDocument(dataRoot, A, "mission.json");
assert.equal(tree(dataRoot), before);
});
+131
View File
@@ -0,0 +1,131 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { RunsError, readActivationLog, readActivePointer } from "../src/index.mjs";
import { scratch, tree, write } from "./helpers.mjs";
const POINTER = { pointerVersion: 1, release: "0.0.12", imageTag: "mosaic-poc-agent:0.84.4-r0.0.12", activatedAt: "2026-09-03T20:58:15Z" };
function entry(at, event, release, extra = {}) {
return { at, event, release, imageTag: `mosaic-poc-agent:0.84.4-r${release}`, ...extra };
}
function writeLog(dataRoot, lines) {
write(path.join(dataRoot, "state", "activation-log.jsonl"), lines.map((l) => (typeof l === "string" ? l : JSON.stringify(l))).join("\n") + "\n");
}
test("no pointer is null", (t) => {
const { dir, dataRoot } = scratch(t);
assert.equal(readActivePointer(dataRoot), null);
assert.equal(readActivePointer(path.join(dir, "absent")), null);
});
test("the pointer release.sh writes reads back", (t) => {
const { dataRoot } = scratch(t);
// The exact bytes of release.sh's printf.
write(path.join(dataRoot, "state", "active.json"),
'{"pointerVersion":1,"release":"0.0.12","imageTag":"mosaic-poc-agent:0.84.4-r0.0.12","activatedAt":"2026-09-03T20:58:15Z"}\n');
assert.deepEqual(readActivePointer(dataRoot), POINTER);
});
test("a pointer that isn't the version 1 shape refuses with exit 2", (t) => {
const { dataRoot } = scratch(t);
const file = path.join(dataRoot, "state", "active.json");
const cases = [
["{ truncated", /not valid JSON/],
["[]", /must be a JSON object/],
["null", /must be a JSON object/],
[{ ...POINTER, extra: 1 }, /unsupported key: "extra"/],
[{ ...POINTER, pointerVersion: 2 }, /unsupported pointerVersion 2/],
[{ ...POINTER, release: "" }, /non-empty string "release"/],
[{ ...POINTER, imageTag: 5 }, /non-empty string "imageTag"/],
[{ pointerVersion: 1, release: "0.0.1", imageTag: "x" }, /non-empty string "activatedAt"/],
];
for (const [content, pattern] of cases) {
write(file, content);
assert.throws(() => readActivePointer(dataRoot), (e) => e instanceof RunsError && e.exitCode === 2 && pattern.test(e.message), String(pattern));
}
});
test("a pointer that is a directory refuses with exit 4", (t) => {
const { dataRoot } = scratch(t);
fs.mkdirSync(path.join(dataRoot, "state", "active.json"), { recursive: true });
assert.throws(() => readActivePointer(dataRoot), (e) => e instanceof RunsError && e.exitCode === 4 && /EISDIR/.test(e.message));
});
test("a state file linked out of the data root refuses", (t) => {
const { dataRoot, outside } = scratch(t);
write(path.join(outside, "active.json"), POINTER);
write(path.join(outside, "log.jsonl"), `${JSON.stringify(entry("a", "activate", "0.0.1"))}\n`);
fs.mkdirSync(path.join(dataRoot, "state"));
fs.symlinkSync(path.join(outside, "active.json"), path.join(dataRoot, "state", "active.json"));
fs.symlinkSync(path.join(outside, "log.jsonl"), path.join(dataRoot, "state", "activation-log.jsonl"));
assert.throws(() => readActivePointer(dataRoot), /resolves outside the data root/);
assert.throws(() => readActivationLog(dataRoot), /resolves outside the data root/);
});
test("a state directory linked out of the data root refuses", (t) => {
const { dataRoot, outside } = scratch(t);
write(path.join(outside, "active.json"), POINTER);
fs.symlinkSync(outside, path.join(dataRoot, "state"));
assert.throws(() => readActivePointer(dataRoot), /state\/active.json resolves outside the data root/);
});
test("a state directory linked out to nothing reads as no release and an empty log", (t) => {
const { dataRoot, outside } = scratch(t);
fs.symlinkSync(path.join(outside, "absent"), path.join(dataRoot, "state"));
assert.equal(readActivePointer(dataRoot), null);
assert.deepEqual(readActivationLog(dataRoot), { entries: [], malformed: 0 });
});
test("a missing log is empty", (t) => {
const { dataRoot } = scratch(t);
assert.deepEqual(readActivationLog(dataRoot), { entries: [], malformed: 0 });
});
test("the log reads oldest first and counts malformed lines", (t) => {
const { dataRoot } = scratch(t);
const good = [
entry("2026-09-03T20:57:00Z", "package", "0.0.12"),
entry("2026-09-03T20:57:47Z", "activate", "0.0.12"),
entry("2026-09-03T20:57:50Z", "refused", "0.0.11", { note: "health check failed (fault-injected)" }),
entry("2026-09-03T20:58:15Z", "rollback", "0.0.11"),
];
writeLog(dataRoot, [
good[0],
"{ torn line",
good[1],
"",
" ",
"[]",
{ ...good[1], extra: true },
{ ...good[1], at: 5 },
{ ...good[1], note: null },
{ at: good[1].at, event: good[1].event, release: good[1].release },
{ ...good[1], release: 5 },
good[2],
good[3],
]);
assert.deepEqual(readActivationLog(dataRoot), { entries: good, malformed: 7 });
assert.deepEqual(readActivationLog(dataRoot, { last: 2 }), { entries: good.slice(2), malformed: 7 });
assert.deepEqual(readActivationLog(dataRoot, { last: 99 }).entries, good);
});
test("last must be a positive integer", (t) => {
const { dataRoot } = scratch(t);
for (const last of [0, -1, 1.5, "2", null]) {
assert.throws(() => readActivationLog(dataRoot, { last }), /last must be a positive integer/, String(last));
}
});
test("the state readers write nothing", (t) => {
const { dataRoot } = scratch(t);
write(path.join(dataRoot, "state", "active.json"), POINTER);
writeLog(dataRoot, [entry("a", "activate", "0.0.1"), "{ bad"]);
const before = tree(dataRoot);
readActivePointer(dataRoot);
readActivationLog(dataRoot);
readActivationLog(dataRoot, { last: 1 });
assert.equal(tree(dataRoot), before);
});
+111
View File
@@ -0,0 +1,111 @@
// scripts/mosaic-task.mjs list and show read through this package. These
// spawn the real script against a seeded data root and a scratch config.
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { RESULT, scratch, write } from "./helpers.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
const A = "r-20260101T000000Z-aaaaaa";
const B = "r-20260101T000100Z-bbbbbb";
function task(t, dataRoot, ...args) {
const config = path.join(path.dirname(dataRoot), "config.json");
write(config, { configVersion: 1, environment: "development", dataRoot, execution: { backend: "docker", provider: "zai", model: "m" } });
const env = { ...process.env, MOSAIC_CONFIG: config };
delete env.NODE_TEST_CONTEXT;
const proc = spawnSync(process.execPath, [path.join(ROOT, "scripts", "mosaic-task.mjs"), ...args], { cwd: ROOT, env, encoding: "utf8" });
return { status: proc.status, stdout: proc.stdout, stderr: proc.stderr };
}
test("list prints each run in the established format", (t) => {
const { dataRoot } = scratch(t);
write(path.join(dataRoot, "runs", A, "result.json"), { ...RESULT, workspace: "ws1", session: "s1" });
write(path.join(dataRoot, "runs", B, "task.json"), { id: "t" });
const out = task(t, dataRoot, "list");
assert.equal(out.status, 0, out.stderr);
assert.equal(out.stdout,
`${A} succeeded task=t-one ws=ws1 session=s1\n` +
`${B} unknown task=- ws=- session=-\n`);
});
test("show prints the run in the established format", (t) => {
const { dataRoot } = scratch(t);
const dir = path.join(dataRoot, "runs", A);
write(path.join(dir, "result.json"), { ...RESULT, missionId: "m", tools: ["read"], expectedExact: "hi" });
write(path.join(dir, "mission.json"), { id: "m", objective: "obj" });
const out = task(t, dataRoot, "show", A);
assert.equal(out.status, 0, out.stderr);
assert.equal(out.stdout, [
`run: ${A}`,
"status: succeeded",
"task: t-one",
"mission: m",
"tools: read",
"adapter: (see config) provider=zai model=m",
'request: "hi"',
'response: "hi"',
'expected: "hi"',
"timing: 2026-01-01T00:00:00.000Z -> 2026-01-01T00:00:01.000Z (1000 ms)",
"exit: 0",
"mission snapshot: m - obj",
`artifacts: ${fs.readdirSync(dir).join(", ")}`,
"",
].join("\n"));
});
test("show of a missing run and an invalid id exit 4 as before", (t) => {
const { dataRoot } = scratch(t);
const missing = task(t, dataRoot, "show", A);
assert.equal(missing.status, 4);
assert.equal(missing.stderr, `mosaic-task: run not found: ${A} (under ${path.join(dataRoot, "runs")})\n`);
const invalid = task(t, dataRoot, "show", "../x");
assert.equal(invalid.status, 4);
assert.equal(invalid.stderr, 'mosaic-task: invalid run id: "../x" (expected r-<id>)\n');
});
test("list and show refuse a runs directory linked out of the data root", (t) => {
const { dataRoot, outside } = scratch(t);
write(path.join(outside, A, "result.json"), RESULT);
fs.symlinkSync(outside, path.join(dataRoot, "runs"));
for (const args of [["list"], ["show", A]]) {
const out = task(t, dataRoot, ...args);
assert.equal(out.status, 4, args.join(" "));
assert.equal(out.stdout, "");
assert.match(out.stderr, /^mosaic-task: .*runs resolves outside the data root/);
}
});
test("show refuses a run linked out of the data root; list reports it unknown", (t) => {
const { dataRoot, outside } = scratch(t);
write(path.join(outside, "result.json"), RESULT);
fs.mkdirSync(path.join(dataRoot, "runs"));
fs.symlinkSync(outside, path.join(dataRoot, "runs", A));
const show = task(t, dataRoot, "show", A);
assert.equal(show.status, 4);
assert.equal(show.stdout, "");
assert.match(show.stderr, /resolves outside the data root/);
const list = task(t, dataRoot, "list");
assert.equal(list.status, 0, list.stderr);
assert.equal(list.stdout, `${A} unknown task=- ws=- session=-\n`);
});
test("list shows an r- name that isn't a valid run id, as before", (t) => {
const { dataRoot } = scratch(t);
write(path.join(dataRoot, "runs", "r-.bad", "result.json"), RESULT);
const out = task(t, dataRoot, "list");
assert.equal(out.status, 0, out.stderr);
assert.equal(out.stdout, "r-.bad succeeded task=t-one ws=- session=-\n");
});
test("show refuses an invalid id before reading the config", (t) => {
const { dir } = scratch(t);
const env = { ...process.env, MOSAIC_CONFIG: path.join(dir, "missing.json") };
delete env.NODE_TEST_CONTEXT;
const proc = spawnSync(process.execPath, [path.join(ROOT, "scripts", "mosaic-task.mjs"), "show", "../x"], { cwd: ROOT, env, encoding: "utf8" });
assert.equal(proc.status, 4, proc.stderr);
assert.equal(proc.stderr, 'mosaic-task: invalid run id: "../x" (expected r-<id>)\n');
});
+18 -27
View File
@@ -35,6 +35,7 @@ import { randomBytes } from "node:crypto";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { BusinessError, validateRoleDocument } from "../packages/business/src/index.mjs";
import { RunsError, isRunId, listRunRecords, readRunRecord } from "../packages/runs/src/index.mjs";
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const RUNS_DIRNAME = "runs";
@@ -457,53 +458,43 @@ function runTask(taskFile, options = {}) {
process.exit(status === "succeeded" ? 0 : 1);
}
function readRuns(read) {
try {
return read();
} catch (error) {
if (error instanceof RunsError) fail(error.exitCode, error.message);
throw error;
}
}
function listRuns() {
const resolved = loadConfig();
const root = runsRoot(resolved);
let entries = [];
try {
entries = fs.readdirSync(root).filter((name) => name.startsWith("r-")).sort();
} catch {
// No runs yet.
}
for (const runId of entries) {
for (const { runId, result } of readRuns(() => listRunRecords(resolved.dataRoot))) {
// An incomplete or unreadable run record (result null) reports as unknown.
let status = "unknown";
let taskId = "-";
let workspace = "-";
let session = "-";
try {
const result = JSON.parse(fs.readFileSync(path.join(root, runId, "result.json"), "utf8"));
if (result) {
status = result.status;
taskId = result.taskId;
workspace = result.workspace ?? "-";
session = result.session ?? "-";
} catch {
// Incomplete run record; report as unknown.
}
process.stdout.write(`${runId} ${status.padEnd(9)} task=${taskId.padEnd(18)} ws=${String(workspace).padEnd(10)} session=${session}\n`);
}
}
function showRun(runId) {
if (!/^r-[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(runId)) {
if (!isRunId(runId)) {
fail(4, `invalid run id: ${JSON.stringify(runId)} (expected r-<id>)`);
}
const resolved = loadConfig();
const dir = path.join(runsRoot(resolved), runId);
if (!fs.existsSync(dir)) {
const record = readRuns(() => readRunRecord(resolved.dataRoot, runId));
if (record === null) {
fail(4, `run not found: ${runId} (under ${runsRoot(resolved)})`);
}
const read = (name) => {
try {
return JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
} catch {
return null;
}
};
const result = read("result.json");
const task = read("task.json");
const mission = read("mission.json");
const { result, task, mission } = record;
process.stdout.write(`run: ${runId}\n`);
if (result) {
@@ -529,7 +520,7 @@ function showRun(runId) {
}
if (task) process.stdout.write(`task snapshot: ${"task.json"} present\n`);
if (mission) process.stdout.write(`mission snapshot: ${mission.id} - ${mission.objective}\n`);
process.stdout.write(`artifacts: ${fs.readdirSync(dir).map((f) => `${f}`).join(", ")}\n`);
process.stdout.write(`artifacts: ${record.artifacts.join(", ")}\n`);
process.exit(0);
}