diff --git a/plugins/account-pool/src/server.test.ts b/plugins/account-pool/src/server.test.ts index 673afb002..20c1b6756 100644 --- a/plugins/account-pool/src/server.test.ts +++ b/plugins/account-pool/src/server.test.ts @@ -3986,6 +3986,75 @@ describe("Account Pool plugin", () => { }, ); + it.each([ + { idleMinutes: 45, reload: false }, + { idleMinutes: 45, reload: true }, + { idleMinutes: 61, reload: false }, + { idleMinutes: 61, reload: true }, + ])( + "retains Claude session affinity after $idleMinutes idle minutes with reload=$reload", + async ({ idleMinutes, reload }) => { + let now = 1_800_000_000_000; + let outage = false; + const attempts: Array = []; + const upstreamFetch: typeof fetch = async (_input, init) => { + const key = new Headers(init?.headers).get("x-api-key"); + attempts.push(key); + return Response.json( + {}, + { status: outage && key === "sk-first" ? 503 : 200 }, + ); + }; + const fixture = await affinityFixture( + "claude", + upstreamFetch, + () => now, + ); + let host = fixture.host; + const send = async (id: string) => { + const response = await host.harness.behavior.fetchHttp( + "POST", + "/v1/messages", + { + headers: authHeaders(fixture.key), + body: claudeBody(id), + }, + ); + expect(response.status).toBe(200); + await response.text(); + return attempts.at(-1); + }; + expect(await send("original")).toBe("sk-first"); + outage = true; + expect(await send("fallback")).toBe("sk-second"); + outage = false; + now += idleMinutes * 60 * 1_000; + if (reload) { + host = await host.harness.lifecycle.reload( + createAccountPoolPlugin({ + fetch: upstreamFetch, + now: () => now, + usageUrl: EMPTY_USAGE_URL, + }), + ); + const service = host.harness.behavior.runService("hub"); + cleanups.push(async () => { + service.controller.abort(); + await service.done; + await host.harness.lifecycle.dispose(); + }); + await vi.waitFor(async () => { + const status = statusSchema.parse( + await host.harness.behavior.callRpc("status.get", null), + ); + expect(status.accepting).toBe(true); + }); + } + expect(await send("original")).toBe("sk-first"); + expect(await send("fresh")).toBe("sk-second"); + }, + ); + it.each(["claude", "codex"] as const)( "runs %s sequentially across conversations and keeps a recovered earlier account as backup", async (provider) => {