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