/*
ADR-0088: Unit tests for JRN-01 journal mission-attribution check in sternsystem.validate.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- ADR-0088: initial unit tests for JRN-01 — op-started records in cache-clone operations/*.jsonl must carry non-empty missionId.
*/
import { test, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runSternsystemValidate } from "./sternsystem-validate.ts";
import { makeInput, makeContext, writeSystemConfig, BASE_SETUP } from "./test-helpers.ts";
let testRoot: string;
let workspaceRoot: string;
let cacheDir: string;
beforeEach(async () => {
testRoot = await mkdtemp(join(tmpdir(), "journal-attribution-test-"));
workspaceRoot = join(testRoot, "workspace");
await mkdir(workspaceRoot, { recursive: true });
await BASE_SETUP(workspaceRoot);
await writeSystemConfig(workspaceRoot, [
{ path: "../systems-cache/test-bundle", storageType: "non-bare" },
]);
cacheDir = join(workspaceRoot, "..", "systems-cache", "test-bundle");
});
afterEach(async () => {
await rm(testRoot, { recursive: true, force: true });
});
async function writeJournal(name: string, records: Array>): Promise {
const operationsDir = join(cacheDir, "operations");
await mkdir(operationsDir, { recursive: true });
const lines = records.map((r) => JSON.stringify(r)).join("\n") + "\n";
await writeFile(join(operationsDir, name), lines, "utf8");
}
function opStarted(opId: string, op: string, missionId: string): Record {
return {
kind: "op-started",
opId,
op,
missionId,
at: "2026-09-21T00:00:00.000Z",
platformVersion: "",
};
}
function jrn01Violations(result: Awaited>) {
return result.data?.violations?.filter((v: { rule: string }) => v.rule === "JRN-01") ?? [];
}
function jrn01Warnings(result: Awaited>) {
return result.data?.warnings?.filter((w: { field: string }) => w.field === "JRN-01") ?? [];
}
test("JRN-01 emitted for op-started with empty missionId", async () => {
await writeJournal("ship-test-m000001.jsonl", [
opStarted("op-1", "leitstand.ship", ""),
{ kind: "op-done", opId: "op-1", at: "2026-09-21T00:01:00.000Z" },
]);
const result = await runSternsystemValidate(makeInput({}), makeContext(workspaceRoot));
const violations = jrn01Violations(result);
expect(
violations.length,
"JRN-01 must fire on op-started with empty missionId — check the missionId guard in checkJournalAttribution",
).toBe(1);
expect(violations[0]?.message).toContain("op-1");
expect(violations[0]?.message).toContain("ship-test-m000001.jsonl");
});
test("JRN-01 emitted for op-started with missing missionId field", async () => {
const rec = opStarted("op-2", "leitstand.ship", "m-1");
delete (rec as { missionId?: string }).missionId;
await writeJournal("ship-test-m000002.jsonl", [rec]);
const result = await runSternsystemValidate(makeInput({}), makeContext(workspaceRoot));
expect(jrn01Violations(result)).toHaveLength(1);
});
test("JRN-01 emitted once per offending op-started record", async () => {
await writeJournal("ship-test-m000003.jsonl", [
opStarted("op-a", "leitstand.ship", ""),
opStarted("op-b", "leitstand.ship", "m-1"),
opStarted("op-c", "leitstand.ship", " "),
]);
const result = await runSternsystemValidate(makeInput({}), makeContext(workspaceRoot));
const violations = jrn01Violations(result);
expect(violations).toHaveLength(2);
expect(violations.map((v) => v.message).join(" ")).toContain("op-a");
expect(violations.map((v) => v.message).join(" ")).toContain("op-c");
});
test("JRN-01 not emitted when all op-started carry missionId", async () => {
await writeJournal("ship-test-m000004.jsonl", [
opStarted("op-1", "leitstand.ship", "test-m000004"),
{ kind: "op-done", opId: "op-1", at: "2026-09-21T00:01:00.000Z" },
]);
const result = await runSternsystemValidate(makeInput({}), makeContext(workspaceRoot));
expect(jrn01Violations(result)).toHaveLength(0);
});
test("JRN-01 not emitted when operations dir is absent", async () => {
const result = await runSternsystemValidate(makeInput({}), makeContext(workspaceRoot));
expect(jrn01Violations(result)).toHaveLength(0);
});
test("unreadable journal produces JRN-01 warning, not violation", async () => {
const operationsDir = join(cacheDir, "operations");
await mkdir(operationsDir, { recursive: true });
await writeFile(join(operationsDir, "corrupt.jsonl"), "{not json\n", "utf8");
const result = await runSternsystemValidate(makeInput({}), makeContext(workspaceRoot));
expect(jrn01Violations(result)).toHaveLength(0);
const warnings = jrn01Warnings(result);
expect(warnings.length).toBeGreaterThanOrEqual(1);
expect(warnings[0]?.message).toContain("corrupt.jsonl");
});