/** * config.test.ts — Unit tests for the configuration module. * * Tests cover: * - Default configuration creation * - Validation of each config field * - Partial and full updates via setConfig * - File persistence (save/load) * - Config file path resolution * - Reset to defaults * - Edge cases (edge values, missing files, invalid JSON) * * @module file-claiming/config.test */ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { assert, createTempDir, cleanupTempDir } from "./test-utils.ts"; // --------------------------------------------------------------------------- // Module under test // --------------------------------------------------------------------------- import { createDefaultConfig, validateConfig, getConfig, setConfig, resetConfig, getConfigFilePath, loadConfigFromFile, saveConfigToFile, } from "../src/config"; // --------------------------------------------------------------------------- // Test: Default configuration // --------------------------------------------------------------------------- function testDefaultConfig() { const def = createDefaultConfig(); assert(typeof def.autoReleaseTTL === "number", "autoReleaseTTL is a number"); assert(def.autoReleaseTTL === 300_000, "Default TTL is 300000ms"); assert(def.releaseOnTurnEnd === true, "Default releaseOnTurnEnd is true"); assert(typeof def.lockDir === "string", "lockDir is a string"); assert(def.lockDir.length > 0, "lockDir is non-empty"); assert(Array.isArray(def.blockedTools), "blockedTools is an array"); assert(def.blockedTools.includes("edit"), "blockedTools includes 'edit'"); assert(def.blockedTools.includes("write"), "blockedTools includes 'write'"); assert(def.showDiagnostics === true, "Default showDiagnostics is true"); const def2 = createDefaultConfig(); assert(def !== def2, "Each call returns a fresh object"); assert( JSON.stringify(def) === JSON.stringify(def2), "Fresh copies have same values", ); console.log("✅ Config: default configuration is correct"); } // --------------------------------------------------------------------------- // Test: Validation // --------------------------------------------------------------------------- function testValidation() { let result = validateConfig({ autoReleaseTTL: 600_000 }); assert(result.valid === true, "Valid autoReleaseTTL passes"); assert(result.errors.length === 0, "No errors for valid config"); result = validateConfig({ releaseOnTurnEnd: false }); assert(result.valid === true, "Valid releaseOnTurnEnd passes"); result = validateConfig({ showDiagnostics: false }); assert(result.valid === true, "Valid showDiagnostics passes"); result = validateConfig({ blockedTools: [] }); assert(result.valid === true, "Empty blockedTools is valid"); result = validateConfig({ blockedTools: ["edit", "write", "bash"] }); assert(result.valid === true, "Multiple blockedTools is valid"); result = validateConfig({ lockDir: "/tmp/locks" }); assert(result.valid === true, "Valid lockDir passes"); result = validateConfig({ autoReleaseTTL: 0 }); assert(result.valid === true, "Zero autoReleaseTTL is valid"); result = validateConfig({ autoReleaseTTL: -1 } as any); assert(result.valid === false, "Negative autoReleaseTTL is invalid"); result = validateConfig({ autoReleaseTTL: "abc" } as any); assert(result.valid === false, "String autoReleaseTTL is invalid"); result = validateConfig({ autoReleaseTTL: NaN } as any); assert(result.valid === false, "NaN autoReleaseTTL is invalid"); result = validateConfig({ releaseOnTurnEnd: "yes" } as any); assert(result.valid === false, "String releaseOnTurnEnd is invalid"); result = validateConfig({ lockDir: "" } as any); assert(result.valid === false, "Empty lockDir is invalid"); result = validateConfig({ lockDir: 123 } as any); assert(result.valid === false, "Number lockDir is invalid"); result = validateConfig({ blockedTools: "edit" } as any); assert(result.valid === false, "String blockedTools is invalid"); result = validateConfig({ blockedTools: [1, 2] } as any); assert(result.valid === false, "Number array blockedTools is invalid"); result = validateConfig({ showDiagnostics: 1 } as any); assert(result.valid === false, "Number showDiagnostics is invalid"); result = validateConfig({ autoReleaseTTL: "bad", releaseOnTurnEnd: "bad", showDiagnostics: "bad", } as any); assert(result.valid === false, "Multiple invalid fields produce errors"); assert(result.errors.length >= 3, "Multiple errors reported"); console.log("✅ Config: validation works correctly"); } // --------------------------------------------------------------------------- // Test: Runtime get/set // --------------------------------------------------------------------------- function testGetSet() { resetConfig(); const def = getConfig(); assert(def.autoReleaseTTL === 300_000, "getConfig returns defaults"); const result = setConfig({ autoReleaseTTL: 600_000 }); assert(result.valid === true, "setConfig with valid value succeeds"); assert(result.errors.length === 0, "No errors"); const updated = getConfig(); assert(updated.autoReleaseTTL === 600_000, "Config value updated"); assert(updated.releaseOnTurnEnd === true, "Other values unchanged"); const fail = setConfig({ autoReleaseTTL: -1 } as any); assert(fail.valid === false, "setConfig with invalid value fails"); assert(fail.errors.length > 0, "Error reported"); const unchanged = getConfig(); assert(unchanged.autoReleaseTTL === 600_000, "Config unchanged on failure"); setConfig({ autoReleaseTTL: 30_000, releaseOnTurnEnd: false, showDiagnostics: false, }); const multi = getConfig(); assert(multi.autoReleaseTTL === 30_000, "Multi-set: TTL updated"); assert( multi.releaseOnTurnEnd === false, "Multi-set: releaseOnTurnEnd updated", ); assert(multi.showDiagnostics === false, "Multi-set: showDiagnostics updated"); resetConfig(); console.log("✅ Config: runtime get/set works correctly"); } // --------------------------------------------------------------------------- // Test: Config file path resolution // --------------------------------------------------------------------------- function testConfigFilePath() { const path = getConfigFilePath(); assert( path.endsWith("config.json"), "Config file path ends with config.json", ); const customPath = getConfigFilePath("/tmp/custom-locks"); assert( customPath.startsWith("/tmp/custom-locks"), "Custom lockDir reflected", ); assert( customPath.endsWith("config.json"), "Custom path ends with config.json", ); console.log("✅ Config: file path resolution works"); } // --------------------------------------------------------------------------- // Test: File persistence (save/load) // --------------------------------------------------------------------------- async function testFilePersistence() { resetConfig(); const tempDir = createTempDir("config-test-"); setConfig({ lockDir: tempDir }); await saveConfigToFile(); const configPath = getConfigFilePath(tempDir); assert(existsSync(configPath), "Config file created on disk"); const raw = readFileSync(configPath, "utf-8"); const parsed = JSON.parse(raw); assert(parsed.autoReleaseTTL === 300_000, "Saved config has correct TTL"); assert( parsed.releaseOnTurnEnd === true, "Saved config has correct releaseOnTurnEnd", ); setConfig({ autoReleaseTTL: 600_000 }); await saveConfigToFile(); const raw2 = readFileSync(configPath, "utf-8"); const parsed2 = JSON.parse(raw2); assert(parsed2.autoReleaseTTL === 600_000, "Updated config saved correctly"); setConfig({ autoReleaseTTL: 1000 }); await loadConfigFromFile(configPath); const afterReload = getConfig(); assert( afterReload.autoReleaseTTL === 600_000, "Loaded config overrides in-memory", ); const nonExistent = join(tempDir, "nonexistent", "config.json"); await loadConfigFromFile(nonExistent); const stillLoaded = getConfig(); assert( stillLoaded.autoReleaseTTL === 600_000, "Non-existent file keeps current config", ); resetConfig(); cleanupTempDir(tempDir); console.log("✅ Config: file persistence works"); } // --------------------------------------------------------------------------- // Test: Load from corrupted file // --------------------------------------------------------------------------- async function testCorruptedFile() { resetConfig(); const tempDir = createTempDir("config-corrupt-"); const configPath = getConfigFilePath(tempDir); mkdirSync(tempDir, { recursive: true }); writeFileSync(configPath, "{invalid json}", "utf-8"); const result = await loadConfigFromFile(configPath); assert(result.autoReleaseTTL === 300_000, "Corrupted file keeps defaults"); writeFileSync( configPath, JSON.stringify({ autoReleaseTTL: "invalid", releaseOnTurnEnd: false }), "utf-8", ); await loadConfigFromFile(configPath); const after = getConfig(); assert( after.autoReleaseTTL === 300_000, "Invalid field skipped, keeps default", ); assert( after.releaseOnTurnEnd === false, "Valid field applied from corrupted file", ); resetConfig(); cleanupTempDir(tempDir); console.log("✅ Config: corrupted file handling works"); } // --------------------------------------------------------------------------- // Test: Reset to defaults // --------------------------------------------------------------------------- function testReset() { setConfig({ autoReleaseTTL: 999_999, showDiagnostics: false }); const modified = getConfig(); assert(modified.autoReleaseTTL === 999_999, "Modified TTL"); resetConfig(); const restored = getConfig(); assert(restored.autoReleaseTTL === 300_000, "Reset restores default TTL"); assert( restored.showDiagnostics === true, "Reset restores default showDiagnostics", ); console.log("✅ Config: reset to defaults works"); } // --------------------------------------------------------------------------- // Test: Config immutability // --------------------------------------------------------------------------- function testImmutability() { resetConfig(); const first = getConfig(); const second = getConfig(); (first as any).autoReleaseTTL = 999_999; assert( second.autoReleaseTTL === 300_000, "getConfig returns independent copies", ); console.log("✅ Config: immutability of getConfig is preserved"); } // --------------------------------------------------------------------------- // Test: Edge values // --------------------------------------------------------------------------- function testEdgeValues() { resetConfig(); let result = setConfig({ autoReleaseTTL: Number.MAX_SAFE_INTEGER }); assert(result.valid === true, "MAX_SAFE_INTEGER TTL is valid"); result = setConfig({ autoReleaseTTL: Infinity }); assert(result.valid === false, "Infinity TTL is invalid"); result = setConfig({ blockedTools: [] }); assert(result.valid === true, "Empty blockedTools is valid"); const longDir = "/" + "a".repeat(1000); result = setConfig({ lockDir: longDir }); assert(result.valid === true, "Long lockDir is valid"); resetConfig(); console.log("✅ Config: edge values handled correctly"); } // --------------------------------------------------------------------------- // Test runner // --------------------------------------------------------------------------- async function runTests() { console.log("Running Config Module Tests\n"); const tests = [ testDefaultConfig, testValidation, testGetSet, testConfigFilePath, testFilePersistence, testCorruptedFile, testReset, testImmutability, testEdgeValues, ]; try { for (const test of tests) { try { await test(); } catch (err) { console.error(`\n❌ Test ${test.name} failed: ${err}`); throw err; } } console.log("\n✅ All config module tests passed!"); } catch (err) { console.error(`\n❌ Test suite failed: ${err}`); process.exit(1); } } runTests();