/*
RFC-0564: Unit tests for the CRDT genome log — append/read round-trip,
signature verification, G-Set merge, and membership view derivation.
- Do not test command handlers — those are integration tests.
- Do not test config loading — those are in config.test.ts.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- Fixtures stay inline and minimal so each case states its own setup.
- RFC-0564: initial implementation — genome log unit tests.
*/
// @ai-invariant fixtures must not couple to internals beyond the public surface under test
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
appendGenomeEntry,
readGenomeLog,
mergeGenomeLogs,
deriveMembershipView,
signGenomeEntry,
verifyGenomeEntry,
getGenomeLogSize,
isGenomeLogSizeWarning,
GENOME_LOG_FILENAME,
} from "./genome-log.ts";
import type { GenomeLogEntry } from "./types.ts";
describe("genome-log", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "swim-genome-test-"));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
describe("appendGenomeEntry + readGenomeLog", () => {
it("should append and read entries round-trip", async () => {
const entry: GenomeLogEntry = {
workshopId: "ws-1",
event: "alive",
timestamp: "2026-07-27T10:00:00Z",
source: "ws-1",
signature: "fake-sig",
};
await appendGenomeEntry(tempDir, entry);
const { entries, skipped } = await readGenomeLog(tempDir);
expect(entries).toHaveLength(1);
expect(entries[0].workshopId).toBe("ws-1");
expect(entries[0].event).toBe("alive");
expect(skipped).toBe(0);
});
it("should handle empty log file", async () => {
const { entries, skipped } = await readGenomeLog(tempDir);
expect(entries).toHaveLength(0);
expect(skipped).toBe(0);
});
it("should skip invalid JSON lines", async () => {
const logPath = join(tempDir, GENOME_LOG_FILENAME);
await writeFile(logPath, "not json\n{bad}\n", "utf8");
const { entries, skipped } = await readGenomeLog(tempDir);
expect(entries).toHaveLength(0);
expect(skipped).toBe(2);
});
});
describe("mergeGenomeLogs", () => {
it("should union two logs with overlapping entries (deduplication)", () => {
const entry1: GenomeLogEntry = {
workshopId: "ws-1",
event: "alive",
timestamp: "2026-07-27T10:00:00Z",
source: "ws-1",
signature: "sig-1",
};
const entry2: GenomeLogEntry = {
workshopId: "ws-2",
event: "alive",
timestamp: "2026-07-27T10:01:00Z",
source: "ws-2",
signature: "sig-2",
};
const entry3: GenomeLogEntry = {
workshopId: "ws-3",
event: "alive",
timestamp: "2026-07-27T10:02:00Z",
source: "ws-3",
signature: "sig-3",
};
const local = [entry1, entry2];
const peer = [entry2, entry3];
const merged = mergeGenomeLogs(local, peer);
expect(merged).toHaveLength(3);
expect(merged.map((e) => e.workshopId).sort()).toEqual(["ws-1", "ws-2", "ws-3"]);
});
it("should handle empty logs", () => {
expect(mergeGenomeLogs([], [])).toHaveLength(0);
expect(
mergeGenomeLogs(
[],
[
{
workshopId: "ws-1",
event: "alive",
timestamp: "2026-07-27T10:00:00Z",
source: "ws-1",
signature: "sig",
},
],
),
).toHaveLength(1);
});
});
describe("deriveMembershipView", () => {
it("should derive membership with latest event per workshop", () => {
const entries: GenomeLogEntry[] = [
{
workshopId: "ws-1",
event: "alive",
timestamp: "2026-07-27T10:00:00Z",
source: "ws-1",
signature: "s1",
},
{
workshopId: "ws-1",
event: "suspect",
timestamp: "2026-07-27T11:00:00Z",
source: "ws-2",
signature: "s2",
},
{
workshopId: "ws-2",
event: "alive",
timestamp: "2026-07-27T10:30:00Z",
source: "ws-2",
signature: "s3",
},
];
const view = deriveMembershipView(entries);
expect(view.total).toBe(2);
expect(view.alive).toBe(1);
expect(view.suspect).toBe(1);
expect(view.dead).toBe(0);
const ws1 = view.members.find((m) => m.workshopId === "ws-1");
expect(ws1?.status).toBe("suspect");
});
it("should handle empty log", () => {
const view = deriveMembershipView([]);
expect(view.total).toBe(0);
expect(view.members).toHaveLength(0);
});
it("should count dead and left statuses", () => {
const entries: GenomeLogEntry[] = [
{
workshopId: "ws-1",
event: "dead",
timestamp: "2026-07-27T10:00:00Z",
source: "ws-2",
signature: "s1",
},
{
workshopId: "ws-2",
event: "left",
timestamp: "2026-07-27T10:01:00Z",
source: "ws-2",
signature: "s2",
},
{
workshopId: "ws-3",
event: "alive",
timestamp: "2026-07-27T10:02:00Z",
source: "ws-3",
signature: "s3",
},
];
const view = deriveMembershipView(entries);
expect(view.dead).toBe(1);
expect(view.alive).toBe(1);
});
});
describe("getGenomeLogSize", () => {
it("should return 0 for missing file", async () => {
const size = await getGenomeLogSize(tempDir);
expect(size).toBe(0);
});
it("should return file size in bytes", async () => {
await appendGenomeEntry(tempDir, {
workshopId: "ws-1",
event: "alive",
timestamp: "2026-07-27T10:00:00Z",
source: "ws-1",
signature: "sig",
});
const size = await getGenomeLogSize(tempDir);
expect(size).toBeGreaterThan(0);
});
});
describe("isGenomeLogSizeWarning", () => {
it("should return false for small sizes", () => {
expect(isGenomeLogSizeWarning(1024)).toBe(false);
});
it("should return true for sizes over 10MB", () => {
expect(isGenomeLogSizeWarning(11 * 1024 * 1024)).toBe(true);
});
});
describe("signGenomeEntry + verifyGenomeEntry", () => {
it("should sign and verify an entry with a real Ed25519 keypair", async () => {
const { generateKeypair } = await import("@warpgogol/werkstatt-shared/passport/sign");
const { privateKeyHex, publicKeyMultibase } = await generateKeypair();
const entry = {
workshopId: "ws-test",
event: "alive" as const,
timestamp: "2026-07-27T10:00:00Z",
source: "ws-test",
};
const signed = await signGenomeEntry(entry, privateKeyHex);
const valid = await verifyGenomeEntry(signed, publicKeyMultibase);
expect(valid).toBe(true);
});
it("should fail verification with wrong public key", async () => {
const { generateKeypair } = await import("@warpgogol/werkstatt-shared/passport/sign");
const keypair1 = await generateKeypair();
const keypair2 = await generateKeypair();
const entry = {
workshopId: "ws-test",
event: "alive" as const,
timestamp: "2026-07-27T10:00:00Z",
source: "ws-test",
};
const signed = await signGenomeEntry(entry, keypair1.privateKeyHex);
const valid = await verifyGenomeEntry(signed, keypair2.publicKeyMultibase);
expect(valid).toBe(false);
});
});
});