Files
2026-07-13 13:39:12 +08:00

381 lines
11 KiB
TypeScript

import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import Database from "better-sqlite3";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
function assertAlmostEqual(actual, expected, epsilon = 1e-9, message = "") {
assert.ok(
Math.abs(actual - expected) <= epsilon,
message || `expected ${actual} to be within ${epsilon} of ${expected}`
);
}
// ─── Test Setup: Single temp dir for whole file (core caches DATA_DIR at first import) ────────
const fileTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-domain-test-"));
const originalDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = fileTmpDir;
async function removeStorageFiles(dir) {
const storage = path.join(dir, "storage.sqlite");
try {
const core = await import("../../src/lib/db/core.ts");
core.resetDbInstance();
} catch {
/* core may not be loaded yet */
}
for (const suffix of ["", "-wal", "-shm", "-journal"]) {
const p = storage + suffix;
try {
if (fs.existsSync(p)) fs.unlinkSync(p);
} catch {}
}
}
beforeEach(async () => {
await removeStorageFiles(fileTmpDir);
});
afterEach(async () => {
const core = await import("../../src/lib/db/core.ts");
core.resetDbInstance();
});
after(() => {
process.env.DATA_DIR = originalDataDir;
if (fs.existsSync(fileTmpDir)) fs.rmSync(fileTmpDir, { recursive: true, force: true });
});
// ─── Fallback Policy Tests ────────────────────────
describe("fallbackPolicy persistence", () => {
it("should register and resolve a fallback chain", async () => {
const { registerFallback, resolveFallbackChain, hasFallback, resetAllFallbacks } =
await import("../../src/domain/fallbackPolicy.ts");
resetAllFallbacks();
registerFallback("gpt-4o", [
{ provider: "openai", priority: 0, enabled: true },
{ provider: "anthropic", priority: 1, enabled: true },
{ provider: "disabled-one", priority: 2, enabled: false },
]);
assert.ok(hasFallback("gpt-4o"));
const chain = resolveFallbackChain("gpt-4o");
assert.equal(chain.length, 2); // disabled-one excluded
assert.equal(chain[0].provider, "openai");
assert.equal(chain[1].provider, "anthropic");
resetAllFallbacks();
});
it("should resolve with exclusions", async () => {
const { registerFallback, resolveFallbackChain, resetAllFallbacks } =
await import("../../src/domain/fallbackPolicy.ts");
resetAllFallbacks();
registerFallback("claude-3", [
{ provider: "anthropic", priority: 0 },
{ provider: "openai", priority: 1 },
]);
const chain = resolveFallbackChain("claude-3", ["anthropic"]);
assert.equal(chain.length, 1);
assert.equal(chain[0].provider, "openai");
resetAllFallbacks();
});
it("should get next fallback correctly", async () => {
const { registerFallback, getNextFallback, resetAllFallbacks } =
await import("../../src/domain/fallbackPolicy.ts");
resetAllFallbacks();
registerFallback("test-model", [
{ provider: "p1", priority: 0 },
{ provider: "p2", priority: 1 },
]);
assert.equal(getNextFallback("test-model"), "p1");
assert.equal(getNextFallback("test-model", ["p1"]), "p2");
assert.equal(getNextFallback("test-model", ["p1", "p2"]), null);
// Non-existing model
assert.equal(getNextFallback("no-model"), null);
resetAllFallbacks();
});
it("should remove fallback chain", async () => {
const { registerFallback, removeFallback, hasFallback, resetAllFallbacks } =
await import("../../src/domain/fallbackPolicy.ts");
resetAllFallbacks();
registerFallback("gpt-4", [{ provider: "openai" }]);
assert.ok(hasFallback("gpt-4"));
removeFallback("gpt-4");
assert.ok(!hasFallback("gpt-4"));
resetAllFallbacks();
});
it("should get all fallback chains", async () => {
const { registerFallback, getAllFallbackChains, resetAllFallbacks } =
await import("../../src/domain/fallbackPolicy.ts");
resetAllFallbacks();
registerFallback("m1", [{ provider: "p1" }]);
registerFallback("m2", [{ provider: "p2" }]);
const all = getAllFallbackChains();
assert.ok("m1" in all);
assert.ok("m2" in all);
resetAllFallbacks();
});
});
// ─── Cost Rules Tests ────────────────────────
describe("costRules persistence", () => {
it("should set and check budget", async () => {
const { setBudget, getBudget, checkBudget, resetCostData } =
await import("../../src/domain/costRules.ts");
resetCostData();
setBudget("key1", { dailyLimitUsd: 10 });
const budget = getBudget("key1");
assert.equal(budget.dailyLimitUsd, 10);
assert.equal(budget.warningThreshold, 0.8);
const check = checkBudget("key1");
assert.ok(check.allowed);
assert.equal(check.dailyLimit, 10);
resetCostData();
});
it("should record cost and check daily total", async () => {
const { setBudget, recordCost, getDailyTotal, checkBudget, resetCostData } =
await import("../../src/domain/costRules.ts");
resetCostData();
setBudget("key2", { dailyLimitUsd: 5 });
recordCost("key2", 3.5);
recordCost("key2", 1.0);
const total = getDailyTotal("key2");
assertAlmostEqual(total, 4.5, 1e-9, `daily total ${total} should equal 4.5 (3.5 + 1.0)`);
// Should still be allowed
const check = checkBudget("key2", 0);
assert.ok(check.allowed);
// Should be denied with additional cost
const checkOver = checkBudget("key2", 1.0);
assert.ok(!checkOver.allowed);
resetCostData();
});
it("should return allowed=true when no budget set", async () => {
const { checkBudget, resetCostData } = await import("../../src/domain/costRules.ts");
resetCostData();
const check = checkBudget("no-budget-key");
assert.ok(check.allowed);
assert.equal(check.dailyLimit, 0);
resetCostData();
});
it("should get cost summary", async () => {
const { setBudget, recordCost, getCostSummary, resetCostData } =
await import("../../src/domain/costRules.ts");
resetCostData();
setBudget("key3", { dailyLimitUsd: 100 });
recordCost("key3", 1.5);
recordCost("key3", 2.5);
const summary = getCostSummary("key3");
assertAlmostEqual(
summary.dailyTotal,
4.0,
1e-9,
`dailyTotal ${summary.dailyTotal} should equal 4.0 (1.5 + 2.5)`
);
assertAlmostEqual(
summary.monthlyTotal,
4.0,
1e-9,
`monthlyTotal ${summary.monthlyTotal} should equal 4.0`
);
assert.equal(summary.budget.dailyLimitUsd, 100);
resetCostData();
});
});
// ─── Lockout Policy Tests ────────────────────────
describe("lockoutPolicy persistence", () => {
it("should track failed attempts and trigger lockout", async () => {
const { recordFailedAttempt, checkLockout, recordSuccess } =
await import("../../src/domain/lockoutPolicy.ts");
const id = "test-ip-" + Date.now();
const config = { maxAttempts: 3, lockoutDurationMs: 5000, attemptWindowMs: 10000 };
// First attempts should not lock
let result = recordFailedAttempt(id, config);
assert.ok(!result.locked);
result = recordFailedAttempt(id, config);
assert.ok(!result.locked);
// Third attempt triggers lockout
result = recordFailedAttempt(id, config);
assert.ok(result.locked);
assert.ok(result.remainingMs > 0);
// Check lockout
const lockCheck = checkLockout(id, config);
assert.ok(lockCheck.locked);
// Clean up
recordSuccess(id);
});
it("should unlock after success", async () => {
const { recordFailedAttempt, recordSuccess, checkLockout } =
await import("../../src/domain/lockoutPolicy.ts");
const id = "test-unlock-" + Date.now();
const config = { maxAttempts: 3, lockoutDurationMs: 5000, attemptWindowMs: 10000 };
recordFailedAttempt(id, config);
recordFailedAttempt(id, config);
recordSuccess(id);
const check = checkLockout(id, config);
assert.ok(!check.locked);
});
it("should force-unlock", async () => {
const { recordFailedAttempt, forceUnlock, checkLockout } =
await import("../../src/domain/lockoutPolicy.ts");
const id = "test-force-" + Date.now();
const config = { maxAttempts: 2, lockoutDurationMs: 60000, attemptWindowMs: 60000 };
recordFailedAttempt(id, config);
recordFailedAttempt(id, config);
const lockCheck = checkLockout(id, config);
assert.ok(lockCheck.locked);
forceUnlock(id);
const afterUnlock = checkLockout(id, config);
assert.ok(!afterUnlock.locked);
});
});
// ─── Circuit Breaker Tests ────────────────────────
describe("circuitBreaker persistence", () => {
it("should open after threshold failures", async () => {
const { CircuitBreaker, STATE } = await import("../../src/shared/utils/circuitBreaker.ts");
const cb = new CircuitBreaker("test-cb-" + Date.now(), { failureThreshold: 3 });
assert.equal(cb.state, STATE.CLOSED);
// Simulate failures
for (let i = 0; i < 3; i++) {
try {
await cb.execute(() => Promise.reject(new Error("fail")));
} catch {
// expected
}
}
assert.equal(cb.state, STATE.OPEN);
assert.ok(!cb.canExecute());
});
it("should reset correctly", async () => {
const { CircuitBreaker, STATE } = await import("../../src/shared/utils/circuitBreaker.ts");
const cb = new CircuitBreaker("test-reset-" + Date.now(), { failureThreshold: 2 });
try {
await cb.execute(() => Promise.reject(new Error("fail")));
} catch {}
try {
await cb.execute(() => Promise.reject(new Error("fail")));
} catch {}
assert.equal(cb.state, STATE.OPEN);
cb.reset();
assert.equal(cb.state, STATE.CLOSED);
assert.equal(cb.failureCount, 0);
assert.ok(cb.canExecute());
});
it("should close on success after half-open", async () => {
const { CircuitBreaker, STATE } = await import("../../src/shared/utils/circuitBreaker.ts");
const cb = new CircuitBreaker("test-halfopen-" + Date.now(), {
failureThreshold: 2,
resetTimeout: 10, // 10ms for test speed
});
// Open the circuit
try {
await cb.execute(() => Promise.reject(new Error("fail")));
} catch {}
try {
await cb.execute(() => Promise.reject(new Error("fail")));
} catch {}
assert.equal(cb.state, STATE.OPEN);
// Wait for resetTimeout
await new Promise((r) => setTimeout(r, 20));
// Should transition to HALF_OPEN and succeed
const result = await cb.execute(() => Promise.resolve("ok"));
assert.equal(result, "ok");
assert.equal(cb.state, STATE.CLOSED);
});
it("registry should return statuses", async () => {
const { getCircuitBreaker, getAllCircuitBreakerStatuses } =
await import("../../src/shared/utils/circuitBreaker.ts");
const name = "reg-test-" + Date.now();
const cb = getCircuitBreaker(name, { failureThreshold: 5 });
assert.ok(cb);
const statuses = getAllCircuitBreakerStatuses();
const found = statuses.find((s) => s.name === name);
assert.ok(found);
assert.equal(found.state, "CLOSED");
});
});