fix(quota): pool PATCH prunes OLD group/provider combos before re-sync (no orphan qtSd/ on switch)

This commit is contained in:
diegosouzapw
2026-06-01 02:35:26 -03:00
parent e694674851
commit e5392a7eaa
2 changed files with 71 additions and 7 deletions

View File

@@ -65,19 +65,35 @@ export async function PATCH(request: Request, { params }: RouteParams): Promise<
}
}
// Combos must be reconciled when the pool's group OR connection set changes,
// since the qtSd/ combo name embeds <groupSlug>/<provider>. Each provider in a
// group is served by at most one pool, so removing this pool's CURRENT (old)
// group+provider combos BEFORE the update — then re-syncing AFTER — cleanly
// drops stale combos and mints the new ones, using the existing per-pool
// helpers. Without the pre-update removal, a group/provider switch would leave
// orphan qtSd/ combos a quota key still sees. Guarded + non-fatal.
const combosNeedResync =
body !== null &&
typeof body === "object" &&
("connectionIds" in body || "groupId" in body);
if (combosNeedResync) {
try {
const { removeQuotaCombosForPool } = await import("@/lib/quota/quotaCombos");
await removeQuotaCombosForPool(id); // pool still has its OLD group/provider here
} catch {
// Guard: combo cleanup failure must never break pool update.
}
}
const pool = updatePool(id, parsed.data);
if (!pool) {
return NextResponse.json(buildErrorBody(404, "Pool not found"), { status: 404 });
}
// Re-sync quota combos when connectionIds was explicitly changed.
// updatePool already fires a guarded sync internally, but we add an explicit
// awaited sync here so the caller's response reflects the updated combo state.
// Mirrors the dynamic-import + non-fatal pattern from groups/[id]/route.ts PATCH.
if (body !== null && typeof body === "object" && "connectionIds" in body) {
if (combosNeedResync) {
try {
const { syncQuotaCombos } = await import("@/lib/quota/quotaCombos");
await syncQuotaCombos(id);
await syncQuotaCombos(id); // pool now has its NEW group/provider
} catch {
// Guard: combo-sync failure must never break pool update callers.
}

View File

@@ -24,7 +24,9 @@ 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 { syncQuotaCombos, removeQuotaCombosForPool } = await import(
"../../src/lib/quota/quotaCombos.ts"
);
const { PoolUpdateSchema } = await import("../../src/shared/schemas/quota.ts");
const { isQuotaModelName, parseQuotaModelName, quotaGroupSlug } = await import(
"../../src/lib/quota/quotaModelNaming.ts"
@@ -178,6 +180,52 @@ test("updatePool with new connectionIds triggers combo re-sync (openrouter → b
assert.deepEqual(reread.connectionIds, [idB], "pool.connectionIds must contain only baidu conn");
});
test("PATCH route sequence (remove→update→sync) prunes OLD-provider combos on switch", async () => {
const group = createGroup("RouteSwitchGroup");
const groupSlug = quotaGroupSlug(group.name);
const connA = await providersDb.createProviderConnection({
provider: "openrouter",
authType: "apikey",
name: "rt-or-conn",
apiKey: "sk-rt-or",
});
const idA = (connA as Record<string, unknown>).id as string;
const connB = await providersDb.createProviderConnection({
provider: "baidu",
authType: "apikey",
name: "rt-baidu-conn",
apiKey: "sk-rt-baidu",
});
const idB = (connB as Record<string, unknown>).id as string;
const pool = poolsDb.createPool({ connectionId: idA, name: "Route Switch", groupId: group.id });
await syncQuotaCombos(pool.id);
assert.ok(
(await listQuotaCombos()).some((c) => parseQuotaModelName(c.name)?.provider === "openrouter"),
"precondition: openrouter combos exist"
);
// Mirror the PATCH route's connection/group-change path exactly:
await removeQuotaCombosForPool(pool.id); // pool still has OLD (openrouter) provider here
poolsDb.updatePool(pool.id, { connectionIds: [idB] });
await syncQuotaCombos(pool.id); // pool now has NEW (baidu) provider
const after = await listQuotaCombos();
const orphans = after.filter((c) => {
const p = parseQuotaModelName(c.name);
return p?.groupSlug === groupSlug && p?.provider === "openrouter";
});
assert.equal(orphans.length, 0, `OLD openrouter combos must be pruned; found ${orphans.length}`);
assert.ok(
after.some((c) => {
const p = parseQuotaModelName(c.name);
return p?.groupSlug === groupSlug && p?.provider === "baidu";
}),
"NEW baidu combos must exist after the switch"
);
});
test("updatePool with groupId persists the new group assignment", () => {
const groupA = createGroup("GroupAlpha");
const groupB = createGroup("GroupBeta");