/*
Unit tests for bordbuch io covering its documented public contract and regression-sensitive edge cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: carried legacy entry — Guard against duplicate mission-close/abort in appendBordbuchEntry.
- RFC-0994: add redactForBordbuch unit tests and integration test for redacted summary storage.
*/
import { test, expect, beforeEach, afterEach } from "vitest";
import fs from "node:fs/promises";
import { existsSync, mkdirSync } from "node:fs";
import path from "node:path";
import os from "node:os";
import {
appendBordbuchEntry,
computeEntryHash,
redactForBordbuch,
containsSensitivePayload,
} from "./bordbuch-io.ts";
import type { BordbuchEntry } from "@warpgogol/werkstatt-engine/schemas";
let testRoot: string;
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,
releaseId: null,
actor: "agent",
summary: overrides.summary,
previousHash: prevHash,
metadata: overrides.metadata,
};
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");
}
beforeEach(async () => {
testRoot = await fs.mkdtemp(path.join(os.tmpdir(), "bordbuch-io-test-"));
tmpDir = path.join(testRoot, "workspace");
await fs.mkdir(tmpDir, { recursive: true });
});
afterEach(async () => {
await fs.rm(testRoot, { recursive: true, force: true });
});
test("appendBordbuchEntry rejects duplicate mission-close for same missionId", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m001",
occurredAt: "2026-07-28T10:00:00.000Z",
summary: "Mission m001 opened",
},
null,
);
const e2 = makeEntry(
{
id: "event-000002",
kind: "mission-close",
missionId: "m001",
occurredAt: "2026-07-28T11:00:00.000Z",
summary: "Mission m001 closed",
},
e1.hash,
);
await writeBordbuch([e1, e2]);
await expect(
appendBordbuchEntry(tmpDir, systemId, "mission-close", "duplicate close", "agent", {
missionId: "m001",
writerRole: "mission",
}),
).rejects.toThrow(/mission-close already exists for mission 'm001'/);
});
test("appendBordbuchEntry rejects duplicate mission-abort for same missionId", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m002",
occurredAt: "2026-07-28T10:00:00.000Z",
summary: "Mission m002 opened",
},
null,
);
const e2 = makeEntry(
{
id: "event-000002",
kind: "mission-abort",
missionId: "m002",
occurredAt: "2026-07-28T11:00:00.000Z",
summary: "Mission m002 aborted",
},
e1.hash,
);
await writeBordbuch([e1, e2]);
await expect(
appendBordbuchEntry(tmpDir, systemId, "mission-abort", "duplicate abort", "agent", {
missionId: "m002",
writerRole: "mission",
}),
).rejects.toThrow(/mission-abort already exists for mission 'm002'/);
});
test("appendBordbuchEntry allows mission-close after mission-abort (different lifecycle)", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m003",
occurredAt: "2026-07-28T10:00:00.000Z",
summary: "Mission m003 opened",
},
null,
);
await writeBordbuch([e1]);
// mission-close when only mission-open exists should succeed
const entry = await appendBordbuchEntry(
tmpDir,
systemId,
"mission-close",
"Mission m003 closed",
"agent",
{ missionId: "m003", writerRole: "mission" },
);
expect(entry.kind).toBe("mission-close");
expect(entry.missionId).toBe("m003");
});
test("appendBordbuchEntry allows mission-close for different missionId", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m004",
occurredAt: "2026-07-28T10:00:00.000Z",
summary: "Mission m004 opened",
},
null,
);
const e2 = makeEntry(
{
id: "event-000002",
kind: "mission-close",
missionId: "m004",
occurredAt: "2026-07-28T11:00:00.000Z",
summary: "Mission m004 closed",
},
e1.hash,
);
await writeBordbuch([e1, e2]);
// mission-close for a different mission should succeed
const entry = await appendBordbuchEntry(
tmpDir,
systemId,
"mission-close",
"Mission m005 closed",
"agent",
{ missionId: "m005", writerRole: "mission" },
);
expect(entry.kind).toBe("mission-close");
expect(entry.missionId).toBe("m005");
});
test("appendBordbuchEntry allows non-mission-close kinds after mission-close", async () => {
const e1 = makeEntry(
{
id: "event-000001",
kind: "mission-open",
missionId: "m006",
occurredAt: "2026-07-28T10:00:00.000Z",
summary: "Mission m006 opened",
},
null,
);
const e2 = makeEntry(
{
id: "event-000002",
kind: "mission-close",
missionId: "m006",
occurredAt: "2026-07-28T11:00:00.000Z",
summary: "Mission m006 closed",
},
e1.hash,
);
await writeBordbuch([e1, e2]);
// sichtpass after mission-close should succeed
const entry = await appendBordbuchEntry(
tmpDir,
systemId,
"sichtpass",
"Sichtpass manifest regenerated",
"agent",
{ writerRole: "nachweis" },
);
expect(entry.kind).toBe("sichtpass");
});
// --- RFC-0994: redactForBordbuch ---
test("redactForBordbuch replaces SSH git URLs with [redacted-git-url]", () => {
const input = "Mirror sync — git@github.com:syrokomskyi/warpgogol-com.git";
const result = redactForBordbuch(input);
expect(result).toBe("Mirror sync — [redacted-git-url]");
});
test("redactForBordbuch replaces HTTPS URLs with credentials", () => {
const input = "Push to https://token@github.com/user/repo.git completed";
const result = redactForBordbuch(input);
expect(result).toBe("Push to [redacted-git-url] completed");
});
test("redactForBordbuch handles multiple git URLs in one string", () => {
const input = "Sync from git@github.com:foo/bar.git to git@gitlab.com:baz/qux.git";
const result = redactForBordbuch(input);
expect(result).toBe("Sync from [redacted-git-url] to [redacted-git-url]");
});
test("redactForBordbuch leaves non-git text unchanged", () => {
const input = "Mission m001 closed — all checks passed";
expect(redactForBordbuch(input)).toBe(input);
});
test("redactForBordbuch prevents false positive sensitive payload on SSH URL", () => {
const summary = "Mirror sync — git@github.com:syrokomskyi/warpgogol-com.git";
const redacted = redactForBordbuch(summary);
expect(containsSensitivePayload(redacted)).toBe(false);
});
test("redactForBordbuch still allows real secrets to be caught after redaction", () => {
const summary = "Token abc123 set on git@github.com:user/repo.git";
const redacted = redactForBordbuch(summary);
expect(containsSensitivePayload(redacted)).toBe(true);
});
test("containsSensitivePayload does not flag digit runs inside hex commit SHAs", () => {
// Regression: sha c7905119099c contains 7905119099, a 10-digit run that
// matched the phone pattern and silently dropped a mirror-sync entry.
const summary = "Mirror sync (push, branch: main) — c7905119099c";
expect(containsSensitivePayload(summary)).toBe(false);
});
test("containsSensitivePayload still flags standalone phone numbers", () => {
expect(containsSensitivePayload("Call +49 761 1234 5678 for details")).toBe(true);
expect(containsSensitivePayload("tel: 0761 1234 5678")).toBe(true);
});
test("appendBordbuchEntry stores redacted summary for SSH URL", async () => {
const entry = await appendBordbuchEntry(
tmpDir,
systemId,
"mirror-sync",
"Mirror sync — git@github.com:syrokomskyi/warpgogol-com.git",
"agent",
{ writerRole: "sternsystem" },
);
expect(entry.summary).toBe("Mirror sync — [redacted-git-url]");
});