/*
RFC-0724: unit tests for deprecated bordbuch kind migration — verifies readBordbuch normalizes release-published to release-ready, and validateBordbuch accepts hashes computed with the deprecated kind.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0724: initial deprecated kind migration tests.
*/
import { test, expect, beforeEach, afterEach } from "vitest";
import fs from "node:fs/promises";
import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
import path from "node:path";
import os from "node:os";
import type { BordbuchEntry } from "@warpgogol/werkstatt-engine/schemas";
import {
computeEntryHash,
readBordbuch,
validateBordbuch,
migrateDeprecatedKind,
DEPRECATED_KIND_MIGRATIONS,
} from "./bordbuch-io.ts";
import { bordbuchEntryKindSchema } from "@warpgogol/werkstatt-engine/schemas";
let tmpDir: string;
const systemId = "test-system";
function makeEntry(
overrides: Partial & Pick,
prevHash: string | null,
): BordbuchEntry {
const base: Omit = {
schemaVersion: "1.0.0",
id: overrides.id,
systemId,
occurredAt: overrides.occurredAt,
kind: overrides.kind,
status: "done",
missionId: overrides.missionId ?? null,
releaseId: overrides.releaseId ?? null,
actor: "agent",
summary: overrides.summary,
previousHash: prevHash,
metadata: overrides.metadata,
};
const hash = computeEntryHash(base);
return { ...base, hash };
}
function makeEntryWithDeprecatedKind(
deprecatedKind: string,
id: string,
prevHash: string | null,
): BordbuchEntry {
const base: Omit = {
schemaVersion: "1.0.0",
id,
systemId,
occurredAt: "2026-08-06T21:32:07.472Z",
kind: deprecatedKind as BordbuchEntry["kind"],
status: "done",
missionId: null,
releaseId: null,
actor: "agent",
summary: "Release test-r000001 published",
previousHash: prevHash,
};
const hash = computeEntryHash(base);
return { ...base, hash };
}
async function writeBordbuch(entries: BordbuchEntry[]): Promise {
const cacheDir = path.join(tmpDir, "..", "systems-cache", systemId);
const bordbuchDir = path.join(cacheDir, "bordbuch");
if (!existsSync(bordbuchDir)) mkdirSync(bordbuchDir, { recursive: true });
const ndjson = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
await fs.writeFile(path.join(bordbuchDir, "events.ndjson"), ndjson, "utf8");
}
let testRoot: string;
beforeEach(async () => {
testRoot = mkdtempSync(path.join(os.tmpdir(), "tmp-bordbuch-migration-"));
tmpDir = path.join(testRoot, "workspace");
mkdirSync(tmpDir, { recursive: true });
});
afterEach(async () => {
await fs.rm(testRoot, { recursive: true, force: true });
});
test("migrateDeprecatedKind maps release-published to release-ready", () => {
expect(migrateDeprecatedKind("release-published")).toBe("release-ready");
});
test("migrateDeprecatedKind returns unknown kinds unchanged", () => {
expect(migrateDeprecatedKind("mission-open")).toBe("mission-open");
expect(migrateDeprecatedKind("unknown-kind")).toBe("unknown-kind");
});
test("DEPRECATED_KIND_MIGRATIONS contains release-published -> release-ready", () => {
expect(DEPRECATED_KIND_MIGRATIONS["release-published"]).toBe("release-ready");
});
test("readBordbuch normalizes deprecated release-published kind", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m001",
occurredAt: "2026-08-06T10:00:00.000Z",
summary: "Mission m001 opened",
},
null,
);
// Create entry with deprecated kind — hash computed with "release-published"
const e2 = makeEntryWithDeprecatedKind("release-published", "event-000002", e1.hash);
await writeBordbuch([e1, e2]);
const entries = await readBordbuch(tmpDir, systemId);
expect(entries).toHaveLength(2);
expect(entries[1].kind).toBe("release-ready");
});
test("validateBordbuch accepts hash computed with deprecated kind", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m001",
occurredAt: "2026-08-06T10:00:00.000Z",
summary: "Mission m001 opened",
},
null,
);
const e2 = makeEntry(
{
id: "event-000002",
kind: "mission-close",
missionId: "m001",
occurredAt: "2026-08-06T11:00:00.000Z",
summary: "Mission m001 closed",
},
e1.hash,
);
// Entry with deprecated kind — hash computed with "release-published"
const e3 = makeEntryWithDeprecatedKind("release-published", "event-000003", e2.hash);
// Subsequent entry chains from e3's hash (which was computed with old kind)
const e4 = makeEntry(
{
id: "event-000004",
kind: "deployment",
occurredAt: "2026-08-06T22:00:00.000Z",
summary: "Deployed to alt",
},
e3.hash,
);
await writeBordbuch([e1, e2, e3, e4]);
const { violations } = await validateBordbuch(tmpDir, systemId);
// Should have zero violations — the deprecated kind hash is accepted
expect(violations).toHaveLength(0);
});
test("validateBordbuch still detects real hash mismatches", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m001",
occurredAt: "2026-08-06T10:00:00.000Z",
summary: "Mission m001 opened",
},
null,
);
// Tampered entry — hash doesn't match any computation
const tampered: BordbuchEntry = {
...e1,
summary: "TAMPERED SUMMARY",
hash: e1.hash, // stale hash from original summary
};
await writeBordbuch([tampered]);
const { violations } = await validateBordbuch(tmpDir, systemId);
expect(violations.some((v) => v.rule === "hash-mismatch")).toBe(true);
});
test("DEPRECATED_KIND_MIGRATIONS: every old kind is NOT in the current enum", () => {
const currentKinds = new Set(bordbuchEntryKindSchema.options);
for (const oldKind of Object.keys(DEPRECATED_KIND_MIGRATIONS)) {
expect(currentKinds.has(oldKind as never)).toBe(false);
}
});
test("DEPRECATED_KIND_MIGRATIONS: every migration target IS in the current enum", () => {
const currentKinds = new Set(bordbuchEntryKindSchema.options);
for (const newKind of Object.values(DEPRECATED_KIND_MIGRATIONS)) {
expect(currentKinds.has(newKind)).toBe(true);
}
});