// vikunja:19/302: a bad organization file keeps the bus down (fail closed) and says why. The status // is written by `bus start` before it throws, readable with the bus down, and cleared by the next // start that loads the file. No test reaches a running bus: a start with a good file stops at the // missing notifier config, after the load was recorded. import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, readFileSync, writeFileSync, statSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { fixture, tmp } from "./helpers.mjs"; import { bootEventsFile, bootLine, bootStatusFile, readBootStatus, recordBootOk, recordBootRefused } from "../src/boot-status.mjs"; import { hostFile } from "../src/host.mjs"; const CLI = fileURLToPath(new URL("../src/cli.mjs", import.meta.url)); const run = (f, ...args) => spawnSync(process.execPath, [CLI, ...args], { env: f.env, encoding: "utf8", timeout: 40000 }); const businessFile = (f) => join(f.config.replace(/config\.json$/, ""), "businesses", "acme.json"); const write = (f, text) => writeFileSync(businessFile(f), text, { mode: 0o600 }); test("recordBootRefused keeps the last good load across refusals; recordBootOk clears the refusal", (t) => { const dataRoot = tmp(t); assert.equal(readBootStatus(dataRoot), null); assert.equal(bootLine(null), "boot: no start recorded"); recordBootRefused(dataRoot, { business: "acme", file: "/c/acme.json", reason: "unknown key", at: "2026-10-15T10:00:00.000Z" }); assert.deepEqual(readBootStatus(dataRoot), { ok: false, at: "2026-10-15T10:00:00.000Z", business: "acme", file: "/c/acme.json", reason: "unknown key", lastGood: null }); recordBootOk(dataRoot, { business: "acme", file: "/c/acme.json", at: "2026-10-15T11:00:00.000Z" }); assert.equal(readBootStatus(dataRoot).ok, true); recordBootRefused(dataRoot, { business: "acme", file: "/c/acme.json", reason: "x".repeat(5000), at: "2026-10-15T12:00:00.000Z" }); recordBootRefused(dataRoot, { business: "acme", file: "/c/acme.json", reason: "still bad", at: "2026-10-15T12:30:00.000Z" }); const s = readBootStatus(dataRoot); assert.deepEqual([s.ok, s.reason, s.lastGood], [false, "still bad", "2026-10-15T11:00:00.000Z"]); assert.match(bootLine(s), /^boot: REFUSED at 2026-10-15T12:30:00\.000Z: bus down, organization file invalid: still bad \(file \/c\/acme\.json; last good load 2026-10-15T11:00:00\.000Z\)$/); const events = readFileSync(bootEventsFile(dataRoot), "utf8").trim().split("\n").map(JSON.parse); assert.deepEqual(events.map((e) => e.event), ["bus.boot.refused", "bus.boot.ok", "bus.boot.refused", "bus.boot.refused"]); assert.equal(events[2].reason.length, 1000); for (const file of [bootStatusFile(dataRoot), bootEventsFile(dataRoot)]) assert.equal(statSync(file).mode & 0o777, 0o600); }); test("an unknown key and a JSON syntax error each leave the bus down with the reason and path in the status", (t) => { const f = fixture(tmp(t)); const file = businessFile(f); const doc = JSON.parse(readFileSync(file, "utf8")); const cases = [ [JSON.stringify({ ...doc, surprise: 1 }), /surprise/], ['{"businessVersion": 1,', /not valid JSON|JSON/i], ]; for (const [text, reason] of cases) { write(f, text); const r = run(f, "bus", "start", "acme"); assert.equal(r.status, 3, r.stderr); // Fail closed: no host, no lock, no socket, no sessions. assert.equal(existsSync(hostFile(f.dataRoot)), false); assert.equal(existsSync(join(f.dataRoot, "bus", "broker.sock")), false); const s = readBootStatus(f.dataRoot); assert.equal(s.ok, false); assert.equal(s.business, "acme"); assert.equal(s.file, file); assert.match(s.reason, reason); assert.ok(s.reason.includes(file) || /surprise/.test(s.reason)); // `bus status` shows it, in text and in JSON. const out = run(f, "bus", "status"); assert.equal(out.status, 0, out.stderr); assert.match(out.stdout, /host: none/); assert.match(out.stdout, /boot: REFUSED at .*bus down, organization file invalid: /); assert.ok(out.stdout.includes(`file ${file}`)); const json = JSON.parse(run(f, "bus", "status", "--json").stdout); assert.equal(json.boot.ok, false); assert.equal(json.boot.reason, s.reason); } }); test("a missing file is recorded too, and a valid file clears the status on the next start", (t) => { const f = fixture(tmp(t)); const file = businessFile(f); const good = readFileSync(file, "utf8"); // A good load is recorded before the start stops at the missing notifier config. assert.equal(run(f, "bus", "start", "acme").status, 3); const first = readBootStatus(f.dataRoot); assert.equal(first.ok, true); write(f, '{"nope":'); assert.equal(run(f, "bus", "start", "acme").status, 3); const bad = readBootStatus(f.dataRoot); assert.equal(bad.ok, false); assert.equal(bad.lastGood, first.at, "the time of the last good load survives the refusal"); write(f, good); const r = run(f, "bus", "start", "acme"); assert.match(r.stderr, /no notifier config/); const cleared = readBootStatus(f.dataRoot); assert.deepEqual([cleared.ok, cleared.reason], [true, null]); assert.match(run(f, "bus", "status").stdout, /boot: organization file loaded at /); });