113 lines
4.5 KiB
TypeScript
113 lines
4.5 KiB
TypeScript
/**
|
|
* TDD regression for #3954: the router keeps selecting rate-limited (429)
|
|
* accounts because the connection cooldown is not respected.
|
|
*
|
|
* Root cause: `rate_limited_until` is a TEXT column, but the Antigravity
|
|
* full-quota path (`setConnectionRateLimitUntil`) writes a raw epoch NUMBER.
|
|
* SQLite TEXT affinity coerces it to a numeric string like "1781696905131.0".
|
|
* The selection filter `isAccountUnavailable` then does
|
|
* `new Date("1781696905131.0")` → Invalid Date → NaN, so `NaN > Date.now()`
|
|
* is false and the still-cooling connection is NOT skipped → client timeouts.
|
|
*
|
|
* The fix hardens the cooldown read predicates to tolerate numeric-epoch
|
|
* strings (in addition to ISO strings / Date / number), without changing the
|
|
* write path. ISO behavior is preserved (regression guard).
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-3954-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const { isAccountUnavailable, getEarliestRateLimitedUntil, filterAvailableAccounts } = await import(
|
|
"../../open-sse/services/accountFallback.ts"
|
|
);
|
|
|
|
test.after(() => {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
const HOUR = 3_600_000;
|
|
|
|
// ── Pure predicate: the exact chokepoint account selection uses ──────────────
|
|
|
|
test("#3954 isAccountUnavailable: numeric-epoch string (future) is treated as unavailable", () => {
|
|
assert.equal(isAccountUnavailable(String(Date.now() + HOUR)), true);
|
|
});
|
|
|
|
test("#3954 isAccountUnavailable: SQLite REAL→TEXT '.0' epoch string (future) is unavailable", () => {
|
|
assert.equal(isAccountUnavailable(`${Date.now() + HOUR}.0`), true);
|
|
});
|
|
|
|
test("#3954 isAccountUnavailable: numeric-epoch string in the past is available again", () => {
|
|
assert.equal(isAccountUnavailable(String(Date.now() - HOUR)), false);
|
|
assert.equal(isAccountUnavailable(`${Date.now() - HOUR}.0`), false);
|
|
});
|
|
|
|
test("#3954 isAccountUnavailable: ISO strings still work (no regression)", () => {
|
|
assert.equal(isAccountUnavailable(new Date(Date.now() + HOUR).toISOString()), true);
|
|
assert.equal(isAccountUnavailable(new Date(Date.now() - HOUR).toISOString()), false);
|
|
});
|
|
|
|
test("#3954 isAccountUnavailable: empty/null/Date inputs behave", () => {
|
|
assert.equal(isAccountUnavailable(null), false);
|
|
assert.equal(isAccountUnavailable(undefined), false);
|
|
assert.equal(isAccountUnavailable(""), false);
|
|
assert.equal(isAccountUnavailable(new Date(Date.now() + HOUR)), true);
|
|
});
|
|
|
|
test("#3954 getEarliestRateLimitedUntil: honors numeric-epoch-string cooldowns", () => {
|
|
const soon = Date.now() + 30_000;
|
|
const earliest = getEarliestRateLimitedUntil([
|
|
{ rateLimitedUntil: String(Date.now() + 90_000) },
|
|
{ rateLimitedUntil: String(soon) },
|
|
]);
|
|
assert.equal(earliest, new Date(soon).toISOString());
|
|
});
|
|
|
|
test("#3954 filterAvailableAccounts: excludes a numeric-epoch-string future cooldown", () => {
|
|
const accts = [
|
|
{ id: "cooling", rateLimitedUntil: String(Date.now() + HOUR) },
|
|
{ id: "healthy", rateLimitedUntil: null },
|
|
];
|
|
const available = filterAvailableAccounts(accts as never);
|
|
assert.deepEqual(
|
|
available.map((a) => a.id),
|
|
["healthy"]
|
|
);
|
|
});
|
|
|
|
// ── End-to-end: the write coercion that triggers the real bug ───────────────
|
|
|
|
test("#3954 setConnectionRateLimitUntil stores a numeric string that selection still honors", async () => {
|
|
const conn = await providersDb.createProviderConnection({
|
|
provider: "antigravity",
|
|
authType: "oauth",
|
|
name: "AG 3954",
|
|
});
|
|
const connId = (conn as { id: string }).id;
|
|
providersDb.setConnectionRateLimitUntil(connId, Date.now() + HOUR);
|
|
|
|
const db = core.getDbInstance() as unknown as {
|
|
prepare: (sql: string) => { get: (id: string) => { rate_limited_until: unknown } | undefined };
|
|
};
|
|
const row = db
|
|
.prepare("SELECT rate_limited_until FROM provider_connections WHERE id = ?")
|
|
.get(connId);
|
|
const stored = row?.rate_limited_until;
|
|
|
|
// It is persisted in the numeric-string form (NOT ISO) — this is the trap.
|
|
assert.ok(
|
|
/^\d+(\.\d+)?$/.test(String(stored)),
|
|
`expected numeric epoch string, got ${String(stored)}`
|
|
);
|
|
// The selection filter must still treat this connection as unavailable.
|
|
assert.equal(isAccountUnavailable(String(stored)), true);
|
|
});
|