diff --git a/src/lib/quota/quotaCombos.ts b/src/lib/quota/quotaCombos.ts index 61a46c65a1..ed625cf8f9 100644 --- a/src/lib/quota/quotaCombos.ts +++ b/src/lib/quota/quotaCombos.ts @@ -11,6 +11,7 @@ */ import { getPool } from "@/lib/db/quotaPools"; +import { getGroupName } from "@/lib/db/quotaGroups"; import { getProviderConnectionById } from "@/lib/db/providers"; import { getCombos, @@ -20,7 +21,7 @@ import { updateCombo, } from "@/lib/db/combos"; import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry"; -import { quotaModelName, parseQuotaModelName, isQuotaModelName, quotaPoolSlug } from "./quotaModelNaming"; +import { quotaModelName, parseQuotaModelName, isQuotaModelName, quotaGroupSlug } from "./quotaModelNaming"; import { createLogger } from "@/shared/utils/logger"; const log = createLogger("quota/quotaCombos"); @@ -34,9 +35,20 @@ const log = createLogger("quota/quotaCombos"); * Returns null when the pool cannot be found. * Individual connection lookups are deferred to syncQuotaCombos so that a * single missing connection does not abort the whole sync. + * + * B4: also resolves the GROUP NAME so that combos are named + * `qtSd//...` instead of `qtSd//...`. + * Falls back to pool.name when the group record is missing. */ async function resolvePoolForSync(poolId: string): Promise<{ - pool: { id: string; connectionId: string; connectionIds: string[]; name: string }; + pool: { + id: string; + connectionId: string; + connectionIds: string[]; + name: string; + groupId: string; + groupName: string; + }; } | null> { const pool = getPool(poolId); if (!pool) return null; @@ -47,7 +59,20 @@ async function resolvePoolForSync(poolId: string): Promise<{ ? pool.connectionIds : [pool.connectionId]; - return { pool: { id: pool.id, connectionId: pool.connectionId, connectionIds, name: pool.name } }; + // B4: resolve the group name for combo naming. + // Fall back to pool.name when the group is missing (legacy / test isolation). + const groupName = getGroupName(pool.groupId) ?? pool.name; + + return { + pool: { + id: pool.id, + connectionId: pool.connectionId, + connectionIds, + name: pool.name, + groupId: pool.groupId, + groupName, + }, + }; } /** @@ -94,7 +119,10 @@ export async function syncQuotaCombos(poolId: string): Promise { } const { pool } = resolved; - const poolSlug = quotaPoolSlug(pool.name); + // B4: use the GROUP name for combo naming (qtSd//...). + // Falls back to pool.name when the group record is missing. + const groupName = pool.groupName; + const groupSlug = quotaGroupSlug(groupName); // D2: build desired names as the UNION across ALL member connections. // A missing connection (no DB row / no provider field) is silently skipped — @@ -121,11 +149,17 @@ export async function syncQuotaCombos(poolId: string): Promise { if (modelIds.length === 0) continue; for (const modelId of modelIds) { - desiredNames.add(quotaModelName(pool.name, provider, modelId)); + // B4: use groupName (not pool.name) as the first arg so combos carry the group slug. + desiredNames.add(quotaModelName(groupName, provider, modelId)); } upsertWork.push({ connId, provider, modelIds }); } + // B4: the pool is single-provider (guard enforced at pool creation). + // Compute the pool's provider so the prune is scoped to group+provider only + // (never touching another provider's combos in the same group). + const poolProvider: string | undefined = upsertWork[0]?.provider; + // Group steps by model across all connections (Task 3 guarantees a single provider). // This produces one combo per model with ALL connections' steps + strategy "fill-first", // fixing the collision where two same-provider connections would overwrite each other. @@ -139,7 +173,8 @@ export async function syncQuotaCombos(poolId: string): Promise { } for (const [modelId, conns] of byModel) { const provider = conns[0].provider; - const comboName = quotaModelName(pool.name, provider, modelId); + // B4: use groupName for the combo name. + const comboName = quotaModelName(groupName, provider, modelId); const steps = conns.map((c) => ({ kind: "model" as const, model: `${provider}/${modelId}`, @@ -157,8 +192,17 @@ export async function syncQuotaCombos(poolId: string): Promise { } } - // Prune stale combos that belong to this pool slug but are no longer in the - // desired set (union across all current connections). + // B4: Prune stale combos that belong to THIS pool's group+provider but are no + // longer in the desired set. CRITICAL: must NOT prune another provider's combos + // in the same group (e.g. syncing openrouter pool must not delete baidu combos). + // + // Prune condition: groupSlug matches AND provider matches AND name not in desiredNames. + // If poolProvider is undefined (no valid connection), skip pruning to be safe. + if (!poolProvider) { + // No valid connections → nothing to prune (can't scope by provider). + return; + } + let allCombos: Awaited> = []; try { allCombos = await getCombos(); @@ -174,9 +218,12 @@ export async function syncQuotaCombos(poolId: string): Promise { const parsed = parseQuotaModelName(name); if (!parsed) continue; - if (parsed.groupSlug !== poolSlug) continue; - // Belongs to this pool slug but not produced by any current connection → prune. + // B4: provider-scoped prune — only prune combos for THIS group+provider. + if (parsed.groupSlug !== groupSlug) continue; + if (parsed.provider !== poolProvider) continue; + + // Belongs to this group+provider but not produced by any current connection → prune. if (!desiredNames.has(name)) { try { await deleteComboByName(name); @@ -217,15 +264,46 @@ export function filterModelsToQuotaPools( /** * Delete ALL `quotaShared-*` combos that belong to the given pool. * - * Used on pool deletion. Because the pool may already be gone from the DB when - * this is called, we look up the pool name first; if missing, we fall back to - * scanning all quota combos and deleting those whose parsed slug matches the - * pool's last-known slug (best-effort via poolId as slug). + * B4: scoped to this pool's group+provider so that removing one pool does not + * accidentally delete another provider's combos that share the same group. + * + * Used on pool deletion. The pool record is looked up to resolve the group + * name and provider. If the pool is already gone from the DB, this is a + * best-effort no-op (nothing to match on provider). */ export async function removeQuotaCombosForPool(poolId: string): Promise { - // Try to get the pool's name to compute the canonical slug - const pool = getPool(poolId); - const slug = pool ? quotaPoolSlug(pool.name) : null; + // Resolve pool → groupName + provider for scoped deletion. + const resolved = await resolvePoolForSync(poolId); + + // If the pool is already gone, we can't safely scope deletion. + // Fall back: try to delete by provider-discovery from DB combos (best-effort). + if (!resolved) { + // Pool gone — nothing to scope by; skip (no partial prune without knowing the provider). + return; + } + + const { pool } = resolved; + const groupName = pool.groupName; + const groupSlug = quotaGroupSlug(groupName); + + // Resolve the pool's provider from its connections. + let poolProvider: string | undefined; + for (const connId of pool.connectionIds) { + try { + const connection = (await getProviderConnectionById(connId)) as Record | null; + if (connection && typeof connection.provider === "string" && connection.provider.length > 0) { + poolProvider = connection.provider; + break; + } + } catch { + // skip + } + } + + if (!poolProvider) { + // Can't scope by provider — skip to avoid nuking unrelated combos. + return; + } let allCombos: Awaited> = []; try { @@ -243,8 +321,9 @@ export async function removeQuotaCombosForPool(poolId: string): Promise { const parsed = parseQuotaModelName(name); if (!parsed) continue; - // Match by slug when we have a pool name; otherwise no match possible - if (slug !== null && parsed.groupSlug !== slug) continue; + // B4: only delete combos for this group+provider. + if (parsed.groupSlug !== groupSlug) continue; + if (parsed.provider !== poolProvider) continue; try { await deleteComboByName(name); diff --git a/tests/unit/quota-combo-balancing.test.ts b/tests/unit/quota-combo-balancing.test.ts index ee5af7a5ab..74b81d1e22 100644 --- a/tests/unit/quota-combo-balancing.test.ts +++ b/tests/unit/quota-combo-balancing.test.ts @@ -122,7 +122,8 @@ test("B1: syncQuotaCombos — 2-connection same-provider pool produces ONE combo // For each model, assert: one combo, 2 steps, fill-first, both connIds present. for (const modelId of modelsForProvider) { - const comboName = quotaModelName(pool.name, PROVIDER, modelId); + // B4: combos are named with the GROUP name ("GroupDemo"), not pool name. + const comboName = quotaModelName("GroupDemo", PROVIDER, modelId); const matchingCombos = quotaCombos.filter((c) => c.name === comboName); // Exactly ONE combo with this name (no duplicate/collision). @@ -332,7 +333,8 @@ test("B5: after syncQuotaCombos on 2-connection pool, getComboByName returns the await syncQuotaCombos(pool.id); - const comboName = quotaModelName(pool.name, PROVIDER, FIRST_MODEL); + // B4: combos are named with the GROUP name ("GroupDemo"), not pool name. + const comboName = quotaModelName("GroupDemo", PROVIDER, FIRST_MODEL); const found = await combosDb.getComboByName(comboName); assert.ok(found, `getComboByName("${comboName}") should return the combo`); diff --git a/tests/unit/quota-combo-cli-providers.test.ts b/tests/unit/quota-combo-cli-providers.test.ts index 6dce83f0bf..a7dd90b5e5 100644 --- a/tests/unit/quota-combo-cli-providers.test.ts +++ b/tests/unit/quota-combo-cli-providers.test.ts @@ -71,7 +71,8 @@ test("syncQuotaCombos generates qtSd/ combos for a CLI provider (codex) via REGI ); assert.ok(quotaCombos.length > 0, "codex pool must produce qtSd/ combos (was 0 before fix)"); - const expectedName = quotaModelName(pool.name, CLI_PROVIDER, firstModelId); + // B4: combos are named with the GROUP name ("GroupDemo" for default group), not pool name. + const expectedName = quotaModelName("GroupDemo", CLI_PROVIDER, firstModelId); const combo = await combosDb.getComboByName(expectedName); assert.ok(combo, `combo "${expectedName}" must exist for the codex pool`); }); diff --git a/tests/unit/quota-combo-groups.test.ts b/tests/unit/quota-combo-groups.test.ts new file mode 100644 index 0000000000..f182fdeb0a --- /dev/null +++ b/tests/unit/quota-combo-groups.test.ts @@ -0,0 +1,303 @@ +/** + * tests/unit/quota-combo-groups.test.ts + * + * Task B4 TDD — Group-aware quotaShared combos. + * + * Two pools in the SAME group produce combos under the GROUP slug + * (`qtSd//...`) and each pool's sync does NOT prune the other + * provider's combos. + * + * Uses "openrouter" (1 model: "auto") as pool A's provider and "baidu" + * (1 model: "ernie-4.0-8k") as pool B's provider — both have a small, + * stable registry entry. + */ + +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-combo-groups-") +); +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 providersDb = await import("../../src/lib/db/providers.ts"); +const combosDb = await import("../../src/lib/db/combos.ts"); +const { createGroup } = await import("../../src/lib/db/quotaGroups.ts"); +const { syncQuotaCombos } = await import("../../src/lib/quota/quotaCombos.ts"); +const { isQuotaModelName, parseQuotaModelName, quotaGroupSlug } = await import( + "../../src/lib/quota/quotaModelNaming.ts" +); + +// --------------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------------- + +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 }); +}); + +// --------------------------------------------------------------------------- +// Helper +// --------------------------------------------------------------------------- + +async function listQuotaCombos(): Promise> { + const all = await combosDb.getCombos(); + return all + .filter((c) => typeof c.name === "string" && isQuotaModelName(c.name as string)) + .map((c) => ({ + name: c.name as string, + models: Array.isArray(c.models) ? (c.models as unknown[]) : [], + })); +} + +// --------------------------------------------------------------------------- +// G1 — Two pools in the SAME group produce combos under the GROUP slug +// --------------------------------------------------------------------------- + +test("G1: two pools in same group → combos named qtSd//provider/model (group slug, not pool name)", async () => { + // Create a named group + const group = createGroup("MyGroup"); + + // Pool A: openrouter + const connA = await providersDb.createProviderConnection({ + provider: "openrouter", + authType: "apikey", + name: "g1-or-conn", + apiKey: "sk-g1-or", + }); + const idA = (connA as Record).id as string; + const poolA = poolsDb.createPool({ + connectionId: idA, + name: "OpenRouter Pool G1", + groupId: group.id, + }); + + // Pool B: baidu + const connB = await providersDb.createProviderConnection({ + provider: "baidu", + authType: "apikey", + name: "g1-baidu-conn", + apiKey: "sk-g1-baidu", + }); + const idB = (connB as Record).id as string; + const poolB = poolsDb.createPool({ + connectionId: idB, + name: "Baidu Pool G1", + groupId: group.id, + }); + + // Sync both pools + await syncQuotaCombos(poolA.id); + await syncQuotaCombos(poolB.id); + + const allCombos = await listQuotaCombos(); + const groupSlug = quotaGroupSlug(group.name); // "mygroup" + + // All combos should use the group slug, not the pool name slug + for (const c of allCombos) { + const parsed = parseQuotaModelName(c.name); + assert.ok(parsed, `Could not parse quota model name: ${c.name}`); + assert.equal( + parsed.groupSlug, + groupSlug, + `Combo "${c.name}" groupSlug should be "${groupSlug}" (group name), got "${parsed.groupSlug}"` + ); + } + + // Combos for openrouter must exist under the group slug + const orCombos = allCombos.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "openrouter"; + }); + assert.ok(orCombos.length > 0, `Expected openrouter combos under qtSd/${groupSlug}/openrouter/...`); + + // Combos for baidu must exist under the group slug + const baiduCombos = allCombos.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "baidu"; + }); + assert.ok(baiduCombos.length > 0, `Expected baidu combos under qtSd/${groupSlug}/baidu/...`); +}); + +// --------------------------------------------------------------------------- +// G2 — Re-syncing pool A does NOT prune pool B's combos (provider-scoped prune) +// --------------------------------------------------------------------------- + +test("G2: re-syncing pool A (openrouter) does not delete pool B (baidu) combos in same group", async () => { + const group = createGroup("SharedGroup"); + const groupSlug = quotaGroupSlug(group.name); + + // Pool A: openrouter + const connA = await providersDb.createProviderConnection({ + provider: "openrouter", + authType: "apikey", + name: "g2-or-conn", + apiKey: "sk-g2-or", + }); + const idA = (connA as Record).id as string; + const poolA = poolsDb.createPool({ + connectionId: idA, + name: "OpenRouter Pool G2", + groupId: group.id, + }); + + // Pool B: baidu + const connB = await providersDb.createProviderConnection({ + provider: "baidu", + authType: "apikey", + name: "g2-baidu-conn", + apiKey: "sk-g2-baidu", + }); + const idB = (connB as Record).id as string; + const poolB = poolsDb.createPool({ + connectionId: idB, + name: "Baidu Pool G2", + groupId: group.id, + }); + + // Sync both + await syncQuotaCombos(poolA.id); + await syncQuotaCombos(poolB.id); + + // Count baidu combos before re-sync of pool A + const beforeResync = await listQuotaCombos(); + const baiduBefore = beforeResync.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "baidu"; + }); + assert.ok(baiduBefore.length > 0, "baidu combos must exist before re-sync"); + + // Re-sync pool A (openrouter) again — must NOT touch baidu combos + await syncQuotaCombos(poolA.id); + + const afterResync = await listQuotaCombos(); + const baiduAfter = afterResync.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "baidu"; + }); + + assert.equal( + baiduAfter.length, + baiduBefore.length, + `Re-syncing pool A (openrouter) must not prune pool B's (baidu) combos. ` + + `Before: ${baiduBefore.length}, After: ${baiduAfter.length}` + ); + + // Also verify openrouter combos still present + const orAfter = afterResync.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "openrouter"; + }); + assert.ok(orAfter.length > 0, "openrouter combos must still be present after re-sync"); +}); + +// --------------------------------------------------------------------------- +// G3 — Pool in default group still works (group name = "GroupDemo", slug = "groupdemo") +// --------------------------------------------------------------------------- + +test("G3: pool in default 'group-demo' group produces combos under groupdemo slug", async () => { + // Create a pool without specifying a groupId → defaults to "group-demo" + const conn = await providersDb.createProviderConnection({ + provider: "glm", + authType: "apikey", + name: "g3-glm-conn", + apiKey: "sk-g3-glm", + }); + const connId = (conn as Record).id as string; + const pool = poolsDb.createPool({ + connectionId: connId, + name: "Default Group Pool", + // no groupId → defaults to "group-demo" + }); + + // Sanity: groupId should be "group-demo" + assert.equal(pool.groupId, "group-demo", "pool without groupId should default to group-demo"); + + await syncQuotaCombos(pool.id); + + const allCombos = await listQuotaCombos(); + assert.ok(allCombos.length > 0, "should produce combos even for default group pool"); + + // All combos should be under the "groupdemo" slug (the group name is "GroupDemo") + for (const c of allCombos) { + const parsed = parseQuotaModelName(c.name); + assert.ok(parsed, `Could not parse: ${c.name}`); + assert.equal( + parsed.groupSlug, + "groupdemo", + `Combo "${c.name}" should be under "groupdemo" slug (GroupDemo group), got "${parsed.groupSlug}"` + ); + } +}); + +// --------------------------------------------------------------------------- +// G4 — Stale prune: a same-group same-provider stale combo IS pruned on re-sync +// --------------------------------------------------------------------------- + +test("G4: stale same-group same-provider combo is pruned on re-sync", async () => { + const group = createGroup("PruneGroup"); + const groupSlug = quotaGroupSlug(group.name); // "prunegroup" + + const conn = await providersDb.createProviderConnection({ + provider: "openrouter", + authType: "apikey", + name: "g4-or-conn", + apiKey: "sk-g4-or", + }); + const connId = (conn as Record).id as string; + const pool = poolsDb.createPool({ + connectionId: connId, + name: "PrunePool G4", + groupId: group.id, + }); + + await syncQuotaCombos(pool.id); + + // Manually insert a stale combo under same group+provider but a nonexistent model + const staleComboName = `qtSd/${groupSlug}/openrouter/fake-stale-model`; + await combosDb.createCombo({ + name: staleComboName, + models: [{ kind: "model", model: "openrouter/fake-stale-model", providerId: "openrouter", weight: 100 }], + strategy: "priority", + isHidden: true, + }); + + const beforePrune = await combosDb.getComboByName(staleComboName); + assert.ok(beforePrune, "stale combo should exist before re-sync"); + + // Re-sync: stale same-group same-provider combo should be pruned + await syncQuotaCombos(pool.id); + + const afterPrune = await combosDb.getComboByName(staleComboName); + assert.equal(afterPrune, null, "stale same-group same-provider combo should be pruned"); +}); diff --git a/tests/unit/quota-combos-sync.test.ts b/tests/unit/quota-combos-sync.test.ts index c022be8cd5..0d3c74c0f6 100644 --- a/tests/unit/quota-combos-sync.test.ts +++ b/tests/unit/quota-combos-sync.test.ts @@ -92,6 +92,7 @@ test("syncQuotaCombos: creates one combo per glm model with correct name and tar const connId = (conn as Record).id as string; assert.ok(connId, "connection should have an id"); + // Pool defaults to "group-demo" (GroupDemo → slug "groupdemo"). const pool = poolsDb.createPool({ connectionId: connId, name: "TestGlmPool" }); await syncQuotaCombos(pool.id); @@ -102,20 +103,21 @@ test("syncQuotaCombos: creates one combo per glm model with correct name and tar const quotaCombos = await listQuotaCombos(); const quotaComboNames = new Set(quotaCombos.map((c) => c.name)); - // Every glm model should have a combo + // B4: combos are named with the GROUP name ("GroupDemo" → slug "groupdemo"), not pool name. + const expectedGroupSlug = quotaPoolSlug("GroupDemo"); for (const model of glmModels) { - const expectedName = quotaModelName("TestGlmPool", "glm", model.id); + const expectedName = quotaModelName("GroupDemo", "glm", model.id); assert.ok( quotaComboNames.has(expectedName), `Missing combo for model ${model.id}: ${expectedName}` ); } - // No extra quota combos for other pools + // All combos should be under the group slug (not the pool name slug). for (const c of quotaCombos) { const parsed = parseQuotaModelName(c.name); assert.ok(parsed, `Could not parse quota model name: ${c.name}`); - assert.equal(parsed?.groupSlug, quotaPoolSlug("TestGlmPool")); + assert.equal(parsed?.groupSlug, expectedGroupSlug); } }); @@ -203,9 +205,9 @@ test("syncQuotaCombos: prunes stale combos for same pool slug", async () => { const initialCount = afterInitial.length; assert.ok(initialCount > 0, "should have combos after initial sync"); - // Manually insert a stale combo with the same pool slug but a nonexistent model - // Use the new qtSd/ prefix so isQuotaModelName() recognises it as a quota combo to prune. - const staleComboName = `qtSd/${quotaPoolSlug("PrunePool")}/glm/fake-model-stale`; + // Manually insert a stale combo with the same group+provider slug but a nonexistent model. + // B4: prune is group+provider scoped. Pool defaults to "group-demo" (GroupDemo → "groupdemo"). + const staleComboName = `qtSd/${quotaPoolSlug("GroupDemo")}/glm/fake-model-stale`; await combosDb.createCombo({ name: staleComboName, models: [{ kind: "model", model: "glm/fake-model-stale", providerId: "glm", weight: 100 }], @@ -253,40 +255,64 @@ test("removeQuotaCombosForPool: removes all quota combos for the pool", async () assert.equal(after.length, 0, "all quota combos should be removed"); }); -test("syncQuotaCombos: does not affect quota combos for a different pool slug", async () => { - const conn = await providersDb.createProviderConnection({ +test("syncQuotaCombos: does not affect quota combos for a different provider in the same group", async () => { + // B4: isolation is now by group+provider (not pool name slug). + // Two pools in the same group (group-demo) but different providers: + // PoolAlpha = glm, PoolBeta = openrouter. Removing PoolAlpha (glm) should + // NOT touch PoolBeta's (openrouter) combos. + const connGlm = await providersDb.createProviderConnection({ provider: "glm", authType: "apikey", - name: "quota-combos-isolation", + name: "quota-combos-isolation-glm", apiKey: "sk-test-glm-isolation", }); - const connId = (conn as Record).id as string; + const connGlmId = (connGlm as Record).id as string; - const poolA = poolsDb.createPool({ connectionId: connId, name: "PoolAlpha" }); - const poolB = poolsDb.createPool({ connectionId: connId, name: "PoolBeta" }); + const connOr = await providersDb.createProviderConnection({ + provider: "openrouter", + authType: "apikey", + name: "quota-combos-isolation-or", + apiKey: "sk-test-or-isolation", + }); + const connOrId = (connOr as Record).id as string; + + // Both pools default to "group-demo" (same group). + const poolA = poolsDb.createPool({ connectionId: connGlmId, name: "PoolAlpha" }); + const poolB = poolsDb.createPool({ connectionId: connOrId, name: "PoolBeta" }); await syncQuotaCombos(poolA.id); await syncQuotaCombos(poolB.id); const all = await listQuotaCombos(); - const slugA = quotaPoolSlug("PoolAlpha"); - const slugB = quotaPoolSlug("PoolBeta"); + const groupSlug = quotaPoolSlug("GroupDemo"); - const forA = all.filter((c) => parseQuotaModelName(c.name)?.groupSlug === slugA); - const forB = all.filter((c) => parseQuotaModelName(c.name)?.groupSlug === slugB); + const forA = all.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "glm"; + }); + const forB = all.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "openrouter"; + }); - assert.ok(forA.length > 0, "PoolAlpha should have combos"); - assert.ok(forB.length > 0, "PoolBeta should have combos"); + assert.ok(forA.length > 0, "PoolAlpha (glm) should have combos"); + assert.ok(forB.length > 0, "PoolBeta (openrouter) should have combos"); - // Removing PoolA's combos should not touch PoolB's + // Removing PoolAlpha (glm) combos should NOT touch PoolBeta's (openrouter) combos. await removeQuotaCombosForPool(poolA.id); const remaining = await listQuotaCombos(); - const remainingForA = remaining.filter((c) => parseQuotaModelName(c.name)?.groupSlug === slugA); - const remainingForB = remaining.filter((c) => parseQuotaModelName(c.name)?.groupSlug === slugB); + const remainingForA = remaining.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "glm"; + }); + const remainingForB = remaining.filter((c) => { + const p = parseQuotaModelName(c.name); + return p?.groupSlug === groupSlug && p?.provider === "openrouter"; + }); - assert.equal(remainingForA.length, 0, "PoolAlpha combos should all be removed"); - assert.equal(remainingForB.length, forB.length, "PoolBeta combos should be untouched"); + assert.equal(remainingForA.length, 0, "PoolAlpha (glm) combos should all be removed"); + assert.equal(remainingForB.length, forB.length, "PoolBeta (openrouter) combos should be untouched"); }); test("syncQuotaCombos: unknown pool id — no throw, prunes nothing (no combos exist)", async () => {