项目文件夹

文件
2026-07-13 13:39:12 +08:00

164 行
6.9 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* tests/unit/quota-division-blocks.test.ts
*
* Task A2 — per-key quota division blocks for countable units.
*
* Regression: before the fix, enforce.ts derived consumedTotal from the
* saturation signal (globalUsedPercent × effectiveLimit), which is always 0
* for countable units (requests/tokens/usd). As a result, the pool was never
* considered "saturated" and enforceQuotaShare never returned "block" for
* countable-unit overage, regardless of actual consumption.
*
* After the fix, enforce.ts uses store.poolConsumedTotal() for countable units,
* which sums real per-key consumption from the store. When the pool total ≥
* effectiveLimit the decision is "block" (global-saturated path in decideFairShare).
*
* Test scenarios (integration through enforceQuotaShare + real SQLite store):
* A. Pool total OVER budget → block (global-saturated fires when
* consumedTotal ≥ effectiveLimit).
* B. Pool total UNDER budget, key UNDER fair-share → allow.
*
* Both scenarios run within a SINGLE test against the SAME SQLite DB using
* distinct connection IDs / pool IDs so their consumptions are fully isolated.
*
* Setup:
* - Custom provider "test-provider-a2" (not in catalog → manual plan only).
* - provider_plans: requests/hourly/limit=100 for each connection.
* - accountCount=1 → effectiveLimit=100; fairShare for 50%-weight key = 50.
* - Pool with 1 connection, 2 API keys at 50/50 hard policy per scenario.
*
* Scenario A: keyA=60, keyB=60 → poolTotal=120 ≥ effectiveLimit=100 → block.
* Scenario B: keyA=20, keyB=20 → poolTotal=40 < effectiveLimit=100 → allow.
*/
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";
// ── Set DATA_DIR BEFORE any DB import (resolved as module-level constant) ───
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-division-blocks-"));
process.env.DATA_DIR = TEST_DATA_DIR;
// ── Imports ──────────────────────────────────────────────────────────────────
const providerPlans = await import("../../../src/lib/db/providerPlans.ts");
const quotaPools = await import("../../../src/lib/db/quotaPools.ts");
const { SqliteQuotaStore } = await import("../../../src/lib/quota/sqliteQuotaStore.ts");
const { enforceQuotaShare } = await import("../../../src/lib/quota/enforce.ts");
const core = await import("../../../src/lib/db/core.ts");
// ── Suite cleanup ─────────────────────────────────────────────────────────────
test.after(() => {
core.resetDbInstance();
if (fs.existsSync(TEST_DATA_DIR)) {
try { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); } catch { /* ignore */ }
}
});
// ── Constants ─────────────────────────────────────────────────────────────────
const LIMIT = 100; // per-account plan limit; accountCount=1 → effectiveLimit=100
const store = new SqliteQuotaStore();
// ─────────────────────────────────────────────────────────────────────────────
// Single suite: both scenarios share a DB, isolated by distinct pool/conn IDs
// ─────────────────────────────────────────────────────────────────────────────
await test("quota-division-blocks: countable-unit enforcement (block + allow)", async (t) => {
// ── Scenario A: pool total > effectiveLimit → block ──────────────────────
await t.test(
"[A] pool total > effectiveLimit → block (global-saturated)",
async () => {
const CONN = "conn-block-a";
const PROV = "test-provider-a2-block";
const KEY_A = "key-block-a1";
const KEY_B = "key-block-b1";
// Seed plan: requests/hourly/limit=100
providerPlans.upsertPlan(
CONN,
PROV,
[{ unit: "requests", window: "hourly", limit: LIMIT }],
"manual"
);
// Create pool: 2 allocations at 50/50 hard
const pool = quotaPools.createPool({
connectionId: CONN,
name: "Block Pool A",
allocations: [
{ apiKeyId: KEY_A, weight: 50, policy: "hard" },
{ apiKeyId: KEY_B, weight: 50, policy: "hard" },
],
});
const dim = { poolId: pool.id, unit: "requests" as const, window: "hourly" as const };
// Consume: keyA=60, keyB=60 → poolTotal=120 > effectiveLimit=100
await store.consume(KEY_A, dim, 60);
await store.consume(KEY_B, dim, 60);
const decision = await enforceQuotaShare({
apiKeyId: KEY_A,
connectionId: CONN,
provider: PROV,
estimatedCost: { requests: 1 },
});
assert.equal(
decision.kind,
"block",
`[A] Expected block when poolTotal(120) ≥ effectiveLimit(100); got: ${JSON.stringify(decision)}`
);
}
);
// ── Scenario B: pool total < effectiveLimit, key under fair-share → allow ─
await t.test(
"[B] pool total < effectiveLimit and key under fair-share → allow",
async () => {
const CONN = "conn-allow-b";
const PROV = "test-provider-a2-allow";
const KEY_A = "key-allow-a1";
const KEY_B = "key-allow-b1";
// Seed plan for separate connection: requests/hourly/limit=100
providerPlans.upsertPlan(
CONN,
PROV,
[{ unit: "requests", window: "hourly", limit: LIMIT }],
"manual"
);
// Create pool: distinct from Scenario A (different poolId + connection)
const pool = quotaPools.createPool({
connectionId: CONN,
name: "Allow Pool B",
allocations: [
{ apiKeyId: KEY_A, weight: 50, policy: "hard" },
{ apiKeyId: KEY_B, weight: 50, policy: "hard" },
],
});
const dim = { poolId: pool.id, unit: "requests" as const, window: "hourly" as const };
// Consume: keyA=20, keyB=20 → poolTotal=40 < effectiveLimit=100
await store.consume(KEY_A, dim, 20);
await store.consume(KEY_B, dim, 20);
const decision = await enforceQuotaShare({
apiKeyId: KEY_A,
connectionId: CONN,
provider: PROV,
estimatedCost: { requests: 1 },
});
assert.equal(
decision.kind,
"allow",
`[B] Expected allow when poolTotal(40) < effectiveLimit(100); got: ${JSON.stringify(decision)}`
);
}
);
});