Files
OmniRoute/tests/unit/quota-per-key-model.test.ts
Diego Rodrigues de Sa e Souza 3d4f3e4960 test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) (#11968)
* test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966)

Two shards on release/v3.8.51 went red in one day with the same signature —
"ENOTEMPTY, Directory not empty: /tmp/omniroute-<test>-XXXXXX" — from
combo-same-provider-cascade (Unit Tests fast-path 4/4, on a PR that touches only
.github/) and auth-policy-embeddings-webfetch-7785 (the 20k-test TIA step). Both pass
alone and on re-run: the cleanup races something still writing into the directory
(SQLite WAL/-shm checkpoint, a worker, the backup) and under a loaded hosted runner
the window opens. 1154 test files do their own cleanup with
fs.rmSync(dir, { recursive: true, force: true }); 57 already asked for retries.

One-shot codemod (scripts/ad-hoc/codemod-rm-maxretries.mjs, kept for the record):
every rm / rmSync / rmdirSync option object with `recursive: true` and no
`maxRetries` gains `maxRetries: 5, retryDelay: 100` — Node itself then retries
ENOTEMPTY/EBUSY/EPERM for up to ~0.5 s before giving up. 2243 call sites in 1292
files under tests/, the shared tests/_setup/isolateDataDir.ts exit hook included.
Only the option object changes: no call site, assertion or import is touched.

Validation: prettier and ESLint (with the frozen suppressions) clean on all 1292
files; a random 20-file sample runs green (quota-redis-store hangs identically on
the untouched tree — it needs a Redis on localhost, an environment matter). The
four unit shards on this PR are the full run.

* fix(quality): let check-forgotten-sibling-tests read a 1,000-file diff

The gate shells out to `git diff` through execFileSync with Node's default 1 MB
maxBuffer; the 1,292-file codemod in this PR is the first diff large enough to
overflow it, and the gate died with `spawnSync git ENOBUFS` before comparing
anything. 64 MB is far above any real PR and costs nothing when unused.
2026-08-29 01:17:40 -03:00

215 lines
7.9 KiB
TypeScript

/**
* tests/unit/quota-per-key-model.test.ts
*
* TDD for per-(key, model) budget/rate-limit cap (Fase 3 #7).
*
* Scenarios:
* 1. keyA has a cap of N requests for model M; after N uses → enforce blocks keyA on model M.
* 2. keyA blocked on M still allowed on model M2 (no cap / cap not reached) in the same pool.
* 3. No cap configured → behaviour unchanged (no block).
* 4. Cap value ≤ EPSILON → ignored (placeholder skip, consistent with planRegistry pattern).
*
* Uses real SQLite (same single-dir reset pattern as db-quota-pools.test.ts).
* Live enforceQuotaShare + recordConsumption path ensures end-to-end correctness.
*
* Part of: Group B — Quota Sharing Engine, Fase 3 #7.
*/
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";
// ── Single isolated DATA_DIR (same pattern as db-quota-pools.test.ts) ────
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-model-cap-"));
process.env.DATA_DIR = TEST_DATA_DIR;
// ── Module imports (after DATA_DIR is set) ────────────────────────────────
const core = await import("../../src/lib/db/core.ts");
const { createPool, upsertAllocations } = await import("../../src/lib/db/quotaPools.ts");
const { setModelCap } = await import("../../src/lib/db/quotaModelCaps.ts");
const { enforceQuotaShare, recordConsumption } = await import("../../src/lib/quota/enforce.ts");
const { resetQuotaStoreSingleton } = await import("../../src/lib/quota/storeFactory.ts");
// ── Storage reset helper (same as db-quota-pools.test.ts) ────────────────
async function resetStorage() {
resetQuotaStoreSingleton();
core.resetDbInstance();
for (let attempt = 0; attempt < 10; attempt++) {
try {
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
break;
} catch (err: unknown) {
const code = (err as NodeJS.ErrnoException)?.code;
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
} else {
throw err;
}
}
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
// ── Test fixtures ─────────────────────────────────────────────────────────
const CONN_ID = "conn-model-cap-test";
const PROVIDER = "kimi"; // kimi has {unit:"requests", window:"hourly", limit:1500} in planRegistry
const KEY_A = "key-model-cap-a";
const MODEL_M = "kimi-k2";
const MODEL_M2 = "kimi-k2-lite";
const CAP_N = 3; // requests
// ── Hooks ─────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
// ── Helper: create pool with KEY_A allocation ─────────────────────────────
function makePool() {
const pool = createPool({ connectionId: CONN_ID, name: "Model Cap Test Pool" });
upsertAllocations(pool.id, [{ apiKeyId: KEY_A, weight: 100, policy: "hard" }]);
return pool;
}
// ---------------------------------------------------------------------------
// Scenario 1: cap N requests on model M → block after N uses
// ---------------------------------------------------------------------------
test("per-(key,model) cap — keyA blocked on model M after N requests", async () => {
const pool = makePool();
setModelCap({
poolId: pool.id,
apiKeyId: KEY_A,
model: MODEL_M,
capValue: CAP_N,
capUnit: "requests",
});
// Simulate CAP_N prior consumptions
for (let i = 0; i < CAP_N; i++) {
await recordConsumption({
apiKeyId: KEY_A,
connectionId: CONN_ID,
provider: PROVIDER,
model: MODEL_M,
cost: { requests: 1 },
});
}
const result = await enforceQuotaShare({
apiKeyId: KEY_A,
connectionId: CONN_ID,
provider: PROVIDER,
model: MODEL_M,
estimatedCost: {},
});
assert.equal(result.kind, "block", "must block when model cap is reached");
assert.ok(
"reason" in result && result.reason.includes("model-cap"),
`reason must mention model-cap; got: ${"reason" in result ? result.reason : "(no reason)"}`
);
assert.equal("httpStatus" in result && result.httpStatus, 429, "must return 429");
});
// ---------------------------------------------------------------------------
// Scenario 2: keyA blocked on M still allowed on M2
// ---------------------------------------------------------------------------
test("per-(key,model) cap — keyA blocked on M, still allowed on M2 same pool", async () => {
const pool = makePool();
setModelCap({
poolId: pool.id,
apiKeyId: KEY_A,
model: MODEL_M,
capValue: 1,
capUnit: "requests",
});
// Consume the single request cap on model M
await recordConsumption({
apiKeyId: KEY_A,
connectionId: CONN_ID,
provider: PROVIDER,
model: MODEL_M,
cost: { requests: 1 },
});
// Model M must be blocked
const resultM = await enforceQuotaShare({
apiKeyId: KEY_A,
connectionId: CONN_ID,
provider: PROVIDER,
model: MODEL_M,
estimatedCost: {},
});
assert.equal(resultM.kind, "block", "model M should be blocked");
assert.ok(
"reason" in resultM && resultM.reason.includes("model-cap"),
`reason must mention model-cap; got: ${"reason" in resultM ? resultM.reason : "(no reason)"}`
);
// Model M2 (no cap configured) must still be allowed
const resultM2 = await enforceQuotaShare({
apiKeyId: KEY_A,
connectionId: CONN_ID,
provider: PROVIDER,
model: MODEL_M2,
estimatedCost: {},
});
assert.equal(resultM2.kind, "allow", "model M2 should still be allowed (no cap on M2)");
});
// ---------------------------------------------------------------------------
// Scenario 3: no cap configured → behaviour unchanged (allow)
// ---------------------------------------------------------------------------
test("per-(key,model) cap — no cap configured → no block (unchanged behaviour)", async () => {
makePool();
// No setModelCap call — cap table is empty
const result = await enforceQuotaShare({
apiKeyId: KEY_A,
connectionId: CONN_ID,
provider: PROVIDER,
model: MODEL_M,
estimatedCost: {},
});
// With a pool but no model cap, the pool-level fair-share check runs.
// KEY_A has weight=100, 0 consumption → well within fair-share → allow.
assert.equal(result.kind, "allow", "no model cap → no block");
});
// ---------------------------------------------------------------------------
// Scenario 4: cap ≤ EPSILON → ignored (placeholder skip)
// ---------------------------------------------------------------------------
test("per-(key,model) cap — EPSILON cap value → ignored, request allowed", async () => {
const pool = makePool();
// Insert a placeholder cap directly (Number.EPSILON > 0 passes DB CHECK constraint
// but enforce.ts skips it: !(capValue > Number.EPSILON) → true for EPSILON).
core
.getDbInstance()
.prepare(
`INSERT INTO quota_allocation_model_caps (pool_id, api_key_id, model, cap_value, cap_unit)
VALUES (?, ?, ?, ?, ?)`
)
.run(pool.id, KEY_A, MODEL_M, Number.EPSILON, "requests");
const result = await enforceQuotaShare({
apiKeyId: KEY_A,
connectionId: CONN_ID,
provider: PROVIDER,
model: MODEL_M,
estimatedCost: {},
});
assert.equal(result.kind, "allow", "EPSILON cap → placeholder → skip → allow");
});