From e9cc53b17fef3e87e4f89b2847f1468bb6864fa0 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 31 May 2026 19:15:16 -0300 Subject: [PATCH] feat(quota): key scope + /v1/models expand to the whole group (real group) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `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//... (naming changed in B3, test was already broken before B5). --- src/lib/db/quotaPools.ts | 13 ++ src/lib/localDb.ts | 1 + src/lib/quota/quotaKey.ts | 88 +++++--- tests/unit/quota-catalog-filter.test.ts | 35 ++-- tests/unit/quota-group-scope.test.ts | 262 ++++++++++++++++++++++++ tests/unit/quota-key-resolve.test.ts | 25 ++- 6 files changed, 377 insertions(+), 47 deletions(-) create mode 100644 tests/unit/quota-group-scope.test.ts diff --git a/src/lib/db/quotaPools.ts b/src/lib/db/quotaPools.ts index e277668648..daca135fed 100644 --- a/src/lib/db/quotaPools.ts +++ b/src/lib/db/quotaPools.ts @@ -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( + "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. */ diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index 532c7941aa..014dec751f 100755 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -539,6 +539,7 @@ export * from "./db/inspectorSessions"; export { listPools, getPool, + getPoolsByGroup, createPool, updatePool, deletePool, diff --git a/src/lib/quota/quotaKey.ts b/src/lib/quota/quotaKey.ts index e493e64f60..ff4d3636b1 100644 --- a/src/lib/quota/quotaKey.ts +++ b/src/lib/quota/quotaKey.ts @@ -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//...` + * 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(); const providerSet = new Set(); - const poolSlugSet = new Set(); + const groupSlugSet = new Set(); + // 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(); 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).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).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//... 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), }; } diff --git a/tests/unit/quota-catalog-filter.test.ts b/tests/unit/quota-catalog-filter.test.ts index 488120d2db..519f66ea03 100644 --- a/tests/unit/quota-catalog-filter.test.ts +++ b/tests/unit/quota-catalog-filter.test.ts @@ -1,20 +1,27 @@ +/** + * Tests for filterModelsToQuotaPools (quota/quotaCombos.ts). + * + * Updated in Task B5 to use the qtSd/// 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" }]); }); }); diff --git a/tests/unit/quota-group-scope.test.ts b/tests/unit/quota-group-scope.test.ts new file mode 100644 index 0000000000..f5e27630bd --- /dev/null +++ b/tests/unit/quota-group-scope.test.ts @@ -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).id as string; + const idB = (connB as Record).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).id as string; + const idH = (connH as Record).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).id as string; + const idB = (connB as Record).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//... 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).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"); +}); diff --git a/tests/unit/quota-key-resolve.test.ts b/tests/unit/quota-key-resolve.test.ts index 4e36b17ece..9c6630fd27 100644 --- a/tests/unit/quota-key-resolve.test.ts +++ b/tests/unit/quota-key-resolve.test.ts @@ -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).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).id as string; const id2 = (conn2 as Record).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).id as string; const idB = (connB as Record).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"]); });