Files
pi-file-claiming/tests/config.test.ts
2026-06-19 12:46:02 -04:00

374 lines
12 KiB
TypeScript

/**
* 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();