mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
feat(quota): key scope + /v1/models expand to the whole group (real group)
- `getPoolsByGroup(groupId)` in quotaPools.ts: SELECT all pools for a group,
exposed as public API and re-exported from localDb.ts.
- `resolveQuotaKeyScope`: pool → group → all pools in group expansion.
The returned `poolSlugs` field now holds GROUP slugs (quotaGroupSlug of the
group name) instead of individual pool-name slugs, one per distinct group.
Group slug included only when the group has ≥1 valid connection (group-level
anyValidConnection gate). Deduplication: a key in 2 pools of the same group
expands once.
- `filterModelsToQuotaPools` (quotaCombos.ts): already matches by
`parsed.groupSlug` ∈ poolSlugs — no logic change needed, just test alignment.
- quota-key-resolve.test.ts: updated 4 tests that asserted pool-name slugs
(testpoola2, poolmix, etc.) to assert the group slug ("groupdemo") — this is
alignment to the B5 group semantics, not masking; the assertions were correct
for the old pool-slug behavior and are now correct for the new group-slug
behavior.
- quota-catalog-filter.test.ts: updated fixtures from old quotaShared-* format
to qtSd/<groupSlug>/... (naming changed in B3, test was already broken before B5).
This commit is contained in:
@@ -206,6 +206,19 @@ function makeId(): string {
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* List all quota pools that belong to a specific group.
|
||||
* Returns an empty array when no pools match the given groupId.
|
||||
*/
|
||||
export function getPoolsByGroup(groupId: string): QuotaPool[] {
|
||||
const rows = getDb()
|
||||
.prepare<PoolRow>(
|
||||
"SELECT id, connection_id, name, group_id, created_at FROM quota_pools WHERE group_id = ? ORDER BY created_at ASC"
|
||||
)
|
||||
.all(groupId);
|
||||
return rows.map((row) => rowToPool(row, getAllocations(row.id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* List all quota pools with their allocations.
|
||||
*/
|
||||
|
||||
@@ -539,6 +539,7 @@ export * from "./db/inspectorSessions";
|
||||
export {
|
||||
listPools,
|
||||
getPool,
|
||||
getPoolsByGroup,
|
||||
createPool,
|
||||
updatePool,
|
||||
deletePool,
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
* `exclusive` flag.
|
||||
*/
|
||||
|
||||
import { getPool } from "@/lib/db/quotaPools";
|
||||
import { getPool, getPoolsByGroup } from "@/lib/db/quotaPools";
|
||||
import { getProviderConnectionById } from "@/lib/db/providers";
|
||||
import { getApiKeyById, updateApiKeyPermissions } from "@/lib/db/apiKeys";
|
||||
import { quotaPoolSlug } from "./quotaModelNaming";
|
||||
import { quotaGroupSlug } from "./quotaModelNaming";
|
||||
import { getGroupName } from "@/lib/db/quotaGroups";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
@@ -21,7 +22,15 @@ export interface QuotaKeyScope {
|
||||
connectionIds: string[];
|
||||
/** Provider slugs of those connections (deduplicated). */
|
||||
providers: string[];
|
||||
/** Alphanumeric pool slugs the key is scoped to (from quotaPoolSlug(pool.name)), deduplicated. */
|
||||
/**
|
||||
* Alphanumeric GROUP slugs the key is scoped to, deduplicated.
|
||||
* Each entry is quotaGroupSlug(groupName) — one per distinct group the
|
||||
* key's allowed pools belong to.
|
||||
*
|
||||
* NOTE: the field is still called `poolSlugs` to minimise churn across
|
||||
* callers; it now holds group slugs (not individual pool-name slugs).
|
||||
* Updated in Task B5 (real-group access).
|
||||
*/
|
||||
poolSlugs: string[];
|
||||
}
|
||||
|
||||
@@ -51,11 +60,24 @@ export function constrainConnectionsToQuota(
|
||||
* returns the set of connection IDs and provider slugs that the key is
|
||||
* permitted to use.
|
||||
*
|
||||
* Task B5 — Real-group access:
|
||||
* For each allowed pool, the scope expands to ALL pools in the same group:
|
||||
* pool → pool.groupId → getPoolsByGroup(groupId) → aggregate their
|
||||
* connections and providers.
|
||||
*
|
||||
* The returned `poolSlugs` field holds GROUP slugs (one per distinct group),
|
||||
* not individual pool-name slugs. This aligns with the `qtSd/<groupSlug>/...`
|
||||
* naming used by filterModelsToQuotaPools and the combo catalog.
|
||||
*
|
||||
* A group slug is only included when the group has at least one valid
|
||||
* connection across any of its pools (same "anyValidConnection" gate,
|
||||
* evaluated group-wide).
|
||||
*
|
||||
* Behaviour:
|
||||
* - Empty / falsy input → `{ connectionIds: [], providers: [] }`.
|
||||
* - Empty / falsy input → `{ connectionIds: [], providers: [], poolSlugs: [] }`.
|
||||
* - Pool IDs that do not resolve (missing pool, missing connection) are
|
||||
* silently skipped — never throws.
|
||||
* - Both arrays are deduplicated; order is not guaranteed.
|
||||
* - All arrays are deduplicated; order is not guaranteed.
|
||||
*/
|
||||
export async function resolveQuotaKeyScope(
|
||||
allowedQuotas: string[] | null | undefined
|
||||
@@ -66,42 +88,56 @@ export async function resolveQuotaKeyScope(
|
||||
|
||||
const connectionIdSet = new Set<string>();
|
||||
const providerSet = new Set<string>();
|
||||
const poolSlugSet = new Set<string>();
|
||||
const groupSlugSet = new Set<string>();
|
||||
|
||||
// Collect the distinct group IDs reachable from the key's allowed pools.
|
||||
// Deduplicate: a key in 2 pools of the same group expands once.
|
||||
const groupIdSet = new Set<string>();
|
||||
for (const poolId of allowedQuotas) {
|
||||
const pool = getPool(poolId);
|
||||
if (!pool) continue;
|
||||
groupIdSet.add(pool.groupId);
|
||||
}
|
||||
|
||||
// D2: iterate ALL member connections (fall back to [connectionId] for any
|
||||
// un-backfilled row where connectionIds is empty/undefined — defensive).
|
||||
const connIds: string[] =
|
||||
Array.isArray(pool.connectionIds) && pool.connectionIds.length > 0
|
||||
? pool.connectionIds
|
||||
: [pool.connectionId];
|
||||
// For each distinct group, aggregate ALL pools in that group.
|
||||
for (const groupId of groupIdSet) {
|
||||
const groupPools = getPoolsByGroup(groupId);
|
||||
// Group-level "anyValidConnection" gate: include the group slug only when
|
||||
// at least one pool in the group has a usable connection.
|
||||
let groupHasValidConnection = false;
|
||||
|
||||
let anyValidConnection = false;
|
||||
for (const connId of connIds) {
|
||||
const connection = await getProviderConnectionById(connId);
|
||||
if (!connection) continue; // missing connection contributes nothing; don't abort
|
||||
for (const groupPool of groupPools) {
|
||||
// D2: iterate ALL member connections (defensive fallback for un-backfilled rows).
|
||||
const connIds: string[] =
|
||||
Array.isArray(groupPool.connectionIds) && groupPool.connectionIds.length > 0
|
||||
? groupPool.connectionIds
|
||||
: [groupPool.connectionId];
|
||||
|
||||
const provider = (connection as Record<string, unknown>).provider;
|
||||
if (typeof provider !== "string" || provider.length === 0) continue;
|
||||
for (const connId of connIds) {
|
||||
const connection = await getProviderConnectionById(connId);
|
||||
if (!connection) continue; // missing connection contributes nothing
|
||||
|
||||
connectionIdSet.add(connId);
|
||||
providerSet.add(provider);
|
||||
anyValidConnection = true;
|
||||
const provider = (connection as Record<string, unknown>).provider;
|
||||
if (typeof provider !== "string" || provider.length === 0) continue;
|
||||
|
||||
connectionIdSet.add(connId);
|
||||
providerSet.add(provider);
|
||||
groupHasValidConnection = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only expose the pool's slug when it has at least one usable connection —
|
||||
// an orphan pool (all connections deleted) has no quotaShared-* models, so
|
||||
// its slug must not leak into the key's scope.
|
||||
if (anyValidConnection) poolSlugSet.add(quotaPoolSlug(pool.name));
|
||||
// Only expose the group slug when the group has at least one usable
|
||||
// connection — a fully-orphaned group has no qtSd/<groupSlug>/... models.
|
||||
if (groupHasValidConnection) {
|
||||
const groupName = getGroupName(groupId) ?? groupId;
|
||||
groupSlugSet.add(quotaGroupSlug(groupName));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connectionIds: Array.from(connectionIdSet),
|
||||
providers: Array.from(providerSet),
|
||||
poolSlugs: Array.from(poolSlugSet),
|
||||
poolSlugs: Array.from(groupSlugSet),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
/**
|
||||
* Tests for filterModelsToQuotaPools (quota/quotaCombos.ts).
|
||||
*
|
||||
* Updated in Task B5 to use the qtSd/<groupSlug>/<provider>/<model> naming
|
||||
* (introduced in B3). The function matches by groupSlug ∈ poolSlugs (which
|
||||
* now holds group slugs after B5).
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { filterModelsToQuotaPools } from "../../src/lib/quota/quotaCombos.js";
|
||||
|
||||
describe("filterModelsToQuotaPools", () => {
|
||||
const models = [
|
||||
{ id: "quotaShared-times-codex/gpt-5.5" },
|
||||
{ id: "quotaShared-times-codex/gpt-5.4" },
|
||||
{ id: "qtSd/times/codex/gpt-5.5" },
|
||||
{ id: "qtSd/times/codex/gpt-5.4" },
|
||||
{ id: "cx/gpt-5.5" },
|
||||
{ id: "quotaShared-other-codex/m" },
|
||||
{ id: "qtSd/other/codex/m" },
|
||||
];
|
||||
|
||||
it("returns only quotaShared-* entries whose poolSlug is in the given pool slugs", () => {
|
||||
it("returns only qtSd/* entries whose groupSlug is in the given slugs", () => {
|
||||
const result = filterModelsToQuotaPools(models, ["times"]);
|
||||
assert.deepEqual(result, [
|
||||
{ id: "quotaShared-times-codex/gpt-5.5" },
|
||||
{ id: "quotaShared-times-codex/gpt-5.4" },
|
||||
{ id: "qtSd/times/codex/gpt-5.5" },
|
||||
{ id: "qtSd/times/codex/gpt-5.4" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -29,28 +36,28 @@ describe("filterModelsToQuotaPools", () => {
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("matches multiple pool slugs simultaneously", () => {
|
||||
it("matches multiple group slugs simultaneously", () => {
|
||||
const result = filterModelsToQuotaPools(models, ["times", "other"]);
|
||||
assert.deepEqual(result, [
|
||||
{ id: "quotaShared-times-codex/gpt-5.5" },
|
||||
{ id: "quotaShared-times-codex/gpt-5.4" },
|
||||
{ id: "quotaShared-other-codex/m" },
|
||||
{ id: "qtSd/times/codex/gpt-5.5" },
|
||||
{ id: "qtSd/times/codex/gpt-5.4" },
|
||||
{ id: "qtSd/other/codex/m" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves extra fields on model entries (generic T extends { id })", () => {
|
||||
const richModels = [
|
||||
{ id: "quotaShared-times-cx/gpt-5.5", object: "model", owned_by: "combo" },
|
||||
{ id: "qtSd/times/cx/gpt-5.5", object: "model", owned_by: "combo" },
|
||||
{ id: "cx/gpt-5.5", object: "model", owned_by: "cx" },
|
||||
];
|
||||
const result = filterModelsToQuotaPools(richModels, ["times"]);
|
||||
assert.deepEqual(result, [
|
||||
{ id: "quotaShared-times-cx/gpt-5.5", object: "model", owned_by: "combo" },
|
||||
{ id: "qtSd/times/cx/gpt-5.5", object: "model", owned_by: "combo" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not match a model from a different pool when only one slug is provided", () => {
|
||||
it("does not match a model from a different group when only one slug is provided", () => {
|
||||
const result = filterModelsToQuotaPools(models, ["other"]);
|
||||
assert.deepEqual(result, [{ id: "quotaShared-other-codex/m" }]);
|
||||
assert.deepEqual(result, [{ id: "qtSd/other/codex/m" }]);
|
||||
});
|
||||
});
|
||||
|
||||
262
tests/unit/quota-group-scope.test.ts
Normal file
262
tests/unit/quota-group-scope.test.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* tests/unit/quota-group-scope.test.ts
|
||||
*
|
||||
* TDD — Task B5: key scope + /v1/models expand to the whole group.
|
||||
*
|
||||
* Scenario: group G has pool A (openrouter, 1 conn) + pool B (baidu, 1 conn).
|
||||
* A key allocated to ONLY pool A:
|
||||
* - resolveQuotaKeyScope([A.id]) must include connectionIds/providers from
|
||||
* BOTH A and B (group-level expansion).
|
||||
* - poolSlugs returns the GROUP slug (quotaGroupSlug of G's name), NOT the
|
||||
* individual pool name slugs.
|
||||
* - filterModelsToQuotaPools keeps models for both providers from that group.
|
||||
*
|
||||
* A key in a DIFFERENT group must NOT see G's models.
|
||||
*
|
||||
* An orphan pool (no valid connections) should NOT add the group slug unless
|
||||
* the group has at least one valid connection (any pool).
|
||||
*/
|
||||
|
||||
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-quota-group-scope-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const poolsDb = await import("../../src/lib/db/quotaPools.ts");
|
||||
const groupsDb = await import("../../src/lib/db/quotaGroups.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const { resolveQuotaKeyScope } = await import("../../src/lib/quota/quotaKey.ts");
|
||||
const { filterModelsToQuotaPools } = await import("../../src/lib/quota/quotaCombos.ts");
|
||||
const { quotaGroupSlug } = await import("../../src/lib/quota/quotaModelNaming.ts");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lifecycle helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function resetStorage() {
|
||||
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 });
|
||||
}
|
||||
break;
|
||||
} catch (error: unknown) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
if ((err?.code === "EBUSY" || err?.code === "EPERM") && attempt < 9) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveQuotaKeyScope: key in pool A sees ALL connections/providers of group G (pool A + pool B)", async () => {
|
||||
// Create group G
|
||||
const groupG = groupsDb.createGroup("GroupG");
|
||||
|
||||
// Create connections for each pool
|
||||
const connA = await providersDb.createProviderConnection({
|
||||
provider: "openrouter",
|
||||
authType: "apikey",
|
||||
name: "conn-openrouter-g",
|
||||
apiKey: "sk-openrouter-g",
|
||||
});
|
||||
const connB = await providersDb.createProviderConnection({
|
||||
provider: "baidu",
|
||||
authType: "apikey",
|
||||
name: "conn-baidu-g",
|
||||
apiKey: "sk-baidu-g",
|
||||
});
|
||||
const idA = (connA as Record<string, unknown>).id as string;
|
||||
const idB = (connB as Record<string, unknown>).id as string;
|
||||
|
||||
// Create pool A and pool B, both in group G
|
||||
const poolA = poolsDb.createPool({ connectionId: idA, name: "Pool A", groupId: groupG.id });
|
||||
const poolB = poolsDb.createPool({ connectionId: idB, name: "Pool B", groupId: groupG.id });
|
||||
|
||||
// Key is allocated ONLY to pool A
|
||||
const scope = await resolveQuotaKeyScope([poolA.id]);
|
||||
|
||||
// Must include both connections
|
||||
assert.ok(scope.connectionIds.includes(idA), "should include pool A connection");
|
||||
assert.ok(scope.connectionIds.includes(idB), "should include pool B connection (group expansion)");
|
||||
assert.equal(scope.connectionIds.length, 2, "exactly 2 connections");
|
||||
|
||||
// Must include both providers
|
||||
assert.ok(scope.providers.includes("openrouter"), "should include openrouter");
|
||||
assert.ok(scope.providers.includes("baidu"), "should include baidu (group expansion)");
|
||||
assert.equal(scope.providers.length, 2, "exactly 2 providers");
|
||||
|
||||
// poolSlugs should be the GROUP slug (not individual pool slugs)
|
||||
const expectedGroupSlug = quotaGroupSlug(groupG.name); // "groupg"
|
||||
assert.deepEqual(scope.poolSlugs, [expectedGroupSlug], "poolSlugs should be the group slug");
|
||||
|
||||
// Ensure pool B's id is reachable from scope (sanity)
|
||||
assert.ok(poolB.id, "pool B exists");
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: key in pool from group H does NOT see group G models", async () => {
|
||||
// Create two groups
|
||||
const groupG = groupsDb.createGroup("GroupG2");
|
||||
const groupH = groupsDb.createGroup("GroupH2");
|
||||
|
||||
const connG = await providersDb.createProviderConnection({
|
||||
provider: "openrouter",
|
||||
authType: "apikey",
|
||||
name: "conn-g2",
|
||||
apiKey: "sk-g2",
|
||||
});
|
||||
const connH = await providersDb.createProviderConnection({
|
||||
provider: "baidu",
|
||||
authType: "apikey",
|
||||
name: "conn-h2",
|
||||
apiKey: "sk-h2",
|
||||
});
|
||||
const idG = (connG as Record<string, unknown>).id as string;
|
||||
const idH = (connH as Record<string, unknown>).id as string;
|
||||
|
||||
const poolInG = poolsDb.createPool({ connectionId: idG, name: "Pool G2", groupId: groupG.id });
|
||||
const poolInH = poolsDb.createPool({ connectionId: idH, name: "Pool H2", groupId: groupH.id });
|
||||
|
||||
// Key only has pool from group H
|
||||
const scope = await resolveQuotaKeyScope([poolInH.id]);
|
||||
|
||||
// Should NOT include group G's connection or provider
|
||||
assert.ok(!scope.connectionIds.includes(idG), "should NOT include group G connection");
|
||||
assert.ok(!scope.providers.includes("openrouter"), "should NOT include openrouter (group G)");
|
||||
|
||||
// Should include group H's connection and provider
|
||||
assert.ok(scope.connectionIds.includes(idH), "should include group H connection");
|
||||
assert.ok(scope.providers.includes("baidu"), "should include baidu (group H)");
|
||||
|
||||
const expectedGroupSlug = quotaGroupSlug(groupH.name);
|
||||
assert.deepEqual(scope.poolSlugs, [expectedGroupSlug], "poolSlugs should be H's group slug");
|
||||
|
||||
// Sanity: pool in G exists but is not seen
|
||||
assert.ok(poolInG.id, "pool in G exists");
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: two pools in the same group expand once (deduplicated group slug)", async () => {
|
||||
const groupG = groupsDb.createGroup("GroupGDedup");
|
||||
|
||||
const connA = await providersDb.createProviderConnection({
|
||||
provider: "openrouter",
|
||||
authType: "apikey",
|
||||
name: "conn-dedup-a",
|
||||
apiKey: "sk-dedup-a",
|
||||
});
|
||||
const connB = await providersDb.createProviderConnection({
|
||||
provider: "baidu",
|
||||
authType: "apikey",
|
||||
name: "conn-dedup-b",
|
||||
apiKey: "sk-dedup-b",
|
||||
});
|
||||
const idA = (connA as Record<string, unknown>).id as string;
|
||||
const idB = (connB as Record<string, unknown>).id as string;
|
||||
|
||||
const poolA = poolsDb.createPool({ connectionId: idA, name: "Pool Dedup A", groupId: groupG.id });
|
||||
const poolB = poolsDb.createPool({ connectionId: idB, name: "Pool Dedup B", groupId: groupG.id });
|
||||
|
||||
// Key has BOTH pools from the same group
|
||||
const scope = await resolveQuotaKeyScope([poolA.id, poolB.id]);
|
||||
|
||||
// Should only appear once in poolSlugs (group-level dedup)
|
||||
const expectedSlug = quotaGroupSlug(groupG.name);
|
||||
assert.deepEqual(scope.poolSlugs, [expectedSlug], "group slug should appear only once");
|
||||
|
||||
// Both connections present
|
||||
assert.ok(scope.connectionIds.includes(idA));
|
||||
assert.ok(scope.connectionIds.includes(idB));
|
||||
assert.equal(scope.connectionIds.length, 2);
|
||||
});
|
||||
|
||||
test("filterModelsToQuotaPools: keeps both providers' qtSd/<group>/... models from scope", async () => {
|
||||
const groupG = groupsDb.createGroup("GroupGFilter");
|
||||
const groupSlug = quotaGroupSlug(groupG.name); // e.g. "groupgfilter"
|
||||
|
||||
const models = [
|
||||
{ id: `qtSd/${groupSlug}/openrouter/gpt-5.5` },
|
||||
{ id: `qtSd/${groupSlug}/baidu/ernie-4.5` },
|
||||
{ id: `qtSd/otherg/openrouter/gpt-5.5` },
|
||||
{ id: "gpt-5.5" }, // not a quota model
|
||||
];
|
||||
|
||||
const result = filterModelsToQuotaPools(models, [groupSlug]);
|
||||
|
||||
assert.equal(result.length, 2, "should return both providers' models for the group");
|
||||
assert.ok(result.some((m) => m.id === `qtSd/${groupSlug}/openrouter/gpt-5.5`));
|
||||
assert.ok(result.some((m) => m.id === `qtSd/${groupSlug}/baidu/ernie-4.5`));
|
||||
assert.ok(!result.some((m) => m.id === `qtSd/otherg/openrouter/gpt-5.5`), "other group filtered out");
|
||||
assert.ok(!result.some((m) => m.id === "gpt-5.5"), "non-quota model filtered out");
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: orphan pool (no valid connections) — group slug excluded if no valid conn in group", async () => {
|
||||
const groupG = groupsDb.createGroup("GroupGOrphan");
|
||||
|
||||
// Pool with a non-existent connection
|
||||
const orphanPool = poolsDb.createPool({
|
||||
connectionId: "conn-does-not-exist-orphan",
|
||||
name: "Orphan Pool G",
|
||||
groupId: groupG.id,
|
||||
});
|
||||
|
||||
const scope = await resolveQuotaKeyScope([orphanPool.id]);
|
||||
|
||||
// No valid connections anywhere in the group → group slug must NOT be included
|
||||
assert.deepEqual(scope.connectionIds, []);
|
||||
assert.deepEqual(scope.providers, []);
|
||||
assert.deepEqual(scope.poolSlugs, [], "group slug excluded when no valid connection in group");
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: orphan pool in group that also has a valid pool — group slug included", async () => {
|
||||
const groupG = groupsDb.createGroup("GroupGPartial");
|
||||
|
||||
// One valid connection pool
|
||||
const connValid = await providersDb.createProviderConnection({
|
||||
provider: "openrouter",
|
||||
authType: "apikey",
|
||||
name: "conn-partial-valid",
|
||||
apiKey: "sk-partial-valid",
|
||||
});
|
||||
const idValid = (connValid as Record<string, unknown>).id as string;
|
||||
const validPool = poolsDb.createPool({ connectionId: idValid, name: "Valid Pool G", groupId: groupG.id });
|
||||
|
||||
// One orphan pool in the same group
|
||||
const orphanPool = poolsDb.createPool({
|
||||
connectionId: "conn-orphan-partial",
|
||||
name: "Orphan Pool G2",
|
||||
groupId: groupG.id,
|
||||
});
|
||||
|
||||
// Key is allocated only to the ORPHAN pool, but the group has the valid pool
|
||||
const scope = await resolveQuotaKeyScope([orphanPool.id]);
|
||||
|
||||
// The group has a valid connection (the validPool's connection) so group slug should be included
|
||||
const expectedSlug = quotaGroupSlug(groupG.name);
|
||||
assert.ok(scope.poolSlugs.includes(expectedSlug), "group slug should be included since group has valid connection");
|
||||
assert.ok(scope.connectionIds.includes(idValid), "should include the valid pool's connection");
|
||||
assert.ok(scope.providers.includes("openrouter"), "should include openrouter from valid pool");
|
||||
|
||||
assert.ok(validPool.id, "valid pool exists");
|
||||
});
|
||||
@@ -80,7 +80,7 @@ test("resolveQuotaKeyScope: unknown pool id returns empty scope (no throw)", asy
|
||||
assert.deepEqual(scope, { connectionIds: [], providers: [], poolSlugs: [] });
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: valid pool returns its connectionId, provider, and poolSlug", async () => {
|
||||
test("resolveQuotaKeyScope: valid pool returns its connectionId, provider, and group slug", async () => {
|
||||
// Seed a real provider connection
|
||||
const conn = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
@@ -91,15 +91,16 @@ test("resolveQuotaKeyScope: valid pool returns its connectionId, provider, and p
|
||||
const connId = (conn as Record<string, unknown>).id as string;
|
||||
assert.ok(connId, "connection should have an id");
|
||||
|
||||
// Seed a pool referencing that connection
|
||||
// Seed a pool referencing that connection (defaults to group-demo)
|
||||
const pool = poolsDb.createPool({ connectionId: connId, name: "Test Pool A2" });
|
||||
|
||||
const scope = await resolveQuotaKeyScope([pool.id]);
|
||||
|
||||
assert.deepEqual(scope.connectionIds, [connId]);
|
||||
assert.deepEqual(scope.providers, ["openai"]);
|
||||
// "Test Pool A2" slugifies to "testpoola2"
|
||||
assert.deepEqual(scope.poolSlugs, ["testpoola2"]);
|
||||
// Task B5: poolSlugs now contains the GROUP slug, not the pool-name slug.
|
||||
// Pool defaults to 'group-demo' → quotaGroupSlug("GroupDemo") = "groupdemo".
|
||||
assert.deepEqual(scope.poolSlugs, ["groupdemo"]);
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: multiple pools same provider deduplicates providers", async () => {
|
||||
@@ -118,6 +119,7 @@ test("resolveQuotaKeyScope: multiple pools same provider deduplicates providers"
|
||||
const id1 = (conn1 as Record<string, unknown>).id as string;
|
||||
const id2 = (conn2 as Record<string, unknown>).id as string;
|
||||
|
||||
// Both pools default to group-demo
|
||||
const pool1 = poolsDb.createPool({ connectionId: id1, name: "Pool Anthro 1" });
|
||||
const pool2 = poolsDb.createPool({ connectionId: id2, name: "Pool Anthro 2" });
|
||||
|
||||
@@ -129,9 +131,12 @@ test("resolveQuotaKeyScope: multiple pools same provider deduplicates providers"
|
||||
|
||||
// provider "anthropic" appears only once even though two pools share it
|
||||
assert.deepEqual(scope.providers, ["anthropic"]);
|
||||
|
||||
// Task B5: both pools are in group-demo → single group slug "groupdemo"
|
||||
assert.deepEqual(scope.poolSlugs, ["groupdemo"]);
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: multiple pools different providers", async () => {
|
||||
test("resolveQuotaKeyScope: multiple pools different providers (same group-demo)", async () => {
|
||||
const connA = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
@@ -147,6 +152,7 @@ test("resolveQuotaKeyScope: multiple pools different providers", async () => {
|
||||
const idA = (connA as Record<string, unknown>).id as string;
|
||||
const idB = (connB as Record<string, unknown>).id as string;
|
||||
|
||||
// Both pools in group-demo (default)
|
||||
const poolA = poolsDb.createPool({ connectionId: idA, name: "Pool OAI" });
|
||||
const poolB = poolsDb.createPool({ connectionId: idB, name: "Pool GEM" });
|
||||
|
||||
@@ -158,6 +164,9 @@ test("resolveQuotaKeyScope: multiple pools different providers", async () => {
|
||||
|
||||
const providers = [...scope.providers].sort();
|
||||
assert.deepEqual(providers, ["gemini", "openai"]);
|
||||
|
||||
// Task B5: both pools are in group-demo → single group slug
|
||||
assert.deepEqual(scope.poolSlugs, ["groupdemo"]);
|
||||
});
|
||||
|
||||
test("resolveQuotaKeyScope: pool referencing non-existent connectionId is skipped gracefully", async () => {
|
||||
@@ -185,6 +194,8 @@ test("resolveQuotaKeyScope: mix of valid and invalid pool ids — only valid con
|
||||
|
||||
assert.deepEqual(scope.connectionIds, [connId]);
|
||||
assert.deepEqual(scope.providers, ["openai"]);
|
||||
// "Pool Mix" slugifies to "poolmix"
|
||||
assert.deepEqual(scope.poolSlugs, ["poolmix"]);
|
||||
// Task B5: poolSlugs now returns the GROUP slug (group-demo → "groupdemo"),
|
||||
// not the pool-name slug ("poolmix"). Ghost pool IDs are skipped (no group
|
||||
// resolution), so only the one valid pool's group appears.
|
||||
assert.deepEqual(scope.poolSlugs, ["groupdemo"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user