/* RFC-0564: Unit tests for SWIM config loading, validation, and creation. Do not test genome log operations — those are in genome-log.test.ts. Fixtures stay inline and minimal so each case states its own setup. RFC-0564: initial implementation — config 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, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadSwimConfig, createSwimConfig, loadOrCreateSwimConfig, validateSwimConfig, validateConfig, CONFIG_FILENAME, } from "./config.ts"; import type { SwimConfig } from "./types.ts"; describe("swim config", () => { let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "swim-config-test-")); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); describe("createSwimConfig", () => { it("should create a valid config with seed and UUID v7 workshopId", async () => { const config = await createSwimConfig(tempDir, "10.0.0.1:7946"); expect(config.workshopId).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i, ); expect(config.bindAddr).toBe("0.0.0.0:7946"); expect(config.seedNodes).toEqual(["10.0.0.1:7946"]); expect(config.probeIntervalMs).toBeGreaterThan(0); expect(config.probeTimeoutMs).toBeGreaterThan(0); expect(config.suspicionTimeoutMs).toBeGreaterThan(0); expect(config.indirectChecks).toBeGreaterThanOrEqual(0); const raw = await readFile(join(tempDir, CONFIG_FILENAME), "utf8"); const parsed = JSON.parse(raw); expect(parsed.workshopId).toBe(config.workshopId); }); }); describe("loadSwimConfig", () => { it("should load a valid config", async () => { const config: SwimConfig = { workshopId: "0197a3b0-1234-7567-89ab-cdef01234567", bindAddr: "0.0.0.0:7946", seedNodes: ["10.0.0.1:7946"], probeIntervalMs: 5000, probeTimeoutMs: 500, suspicionTimeoutMs: 15000, indirectChecks: 3, }; await writeFile(join(tempDir, CONFIG_FILENAME), JSON.stringify(config, null, 2), "utf8"); const loaded = await loadSwimConfig(tempDir); expect(loaded.workshopId).toBe(config.workshopId); expect(loaded.bindAddr).toBe(config.bindAddr); }); it("should throw for missing file", async () => { await expect(loadSwimConfig(tempDir)).rejects.toThrow(); }); it("should throw for invalid config (missing required fields)", async () => { await writeFile( join(tempDir, CONFIG_FILENAME), JSON.stringify({ workshopId: "test" }), "utf8", ); await expect(loadSwimConfig(tempDir)).rejects.toThrow(); }); }); describe("validateConfig", () => { it("should validate a correct config", () => { const config: SwimConfig = { workshopId: "0197a3b0-1234-7567-89ab-cdef01234567", bindAddr: "0.0.0.0:7946", seedNodes: ["10.0.0.1:7946"], probeIntervalMs: 5000, probeTimeoutMs: 500, suspicionTimeoutMs: 15000, indirectChecks: 3, }; expect(() => validateConfig(config)).not.toThrow(); }); it("should throw for non-object config", () => { expect(() => validateConfig(null)).toThrow("must be an object"); expect(() => validateConfig("string")).toThrow("must be an object"); }); it("should throw for empty workshopId", () => { const config = { workshopId: "", bindAddr: "0.0.0.0:7946", seedNodes: [], probeIntervalMs: 5000, probeTimeoutMs: 500, suspicionTimeoutMs: 15000, indirectChecks: 3, }; expect(() => validateConfig(config)).toThrow("workshopId"); }); it("should throw for non-array seedNodes", () => { const config = { workshopId: "test", bindAddr: "0.0.0.0:7946", seedNodes: "not-array", probeIntervalMs: 5000, probeTimeoutMs: 500, suspicionTimeoutMs: 15000, indirectChecks: 3, }; expect(() => validateConfig(config)).toThrow("seedNodes"); }); }); describe("loadOrCreateSwimConfig", () => { it("should load existing config", async () => { const config = await createSwimConfig(tempDir, "10.0.0.1:7946"); const loaded = await loadOrCreateSwimConfig(tempDir, "10.0.0.2:7946"); expect(loaded.workshopId).toBe(config.workshopId); }); it("should create new config when missing and seed provided", async () => { const config = await loadOrCreateSwimConfig(tempDir, "10.0.0.1:7946"); expect(config.workshopId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7/); expect(config.seedNodes).toEqual(["10.0.0.1:7946"]); }); it("should throw when missing and no seed provided", async () => { await expect(loadOrCreateSwimConfig(tempDir)).rejects.toThrow(); }); }); describe("validateSwimConfig", () => { it("should return true for valid config", async () => { await createSwimConfig(tempDir, "10.0.0.1:7946"); expect(await validateSwimConfig(tempDir)).toBe(true); }); it("should return false for missing config", async () => { expect(await validateSwimConfig(tempDir)).toBe(false); }); }); });