374 lines
12 KiB
TypeScript
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();
|