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