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
+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');
});