Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/account-pool/src/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const MAX_REFRESH_BACKOFFS = 1_024;
const UPSTREAM_REJECTION_HOLD_MS = 60_000;
const MAX_FAILURE_DETAIL_BYTES = 1_024;
const FAILURE_DISPOSAL_TIMEOUT_MS = 250;
const AFFINITY_IDLE_TTL_MS = 30 * 60 * 1_000;
const AFFINITY_IDLE_TTL_MS = 65 * 60 * 1_000;
const MAX_AFFINITY_BINDINGS = 4_096;
const DROPPED_RESPONSE_HEADERS = new Set([
"content-encoding",
Expand Down
77 changes: 73 additions & 4 deletions plugins/account-pool/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null> = [];
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) => {
Expand Down Expand Up @@ -4451,7 +4520,7 @@ describe("Account Pool plugin", () => {
"account.disable",
{ id: fixture.account.id },
);
if (reason === "expired") now += 31 * 60 * 1_000;
if (reason === "expired") now += 66 * 60 * 1_000;
const key =
reason === "other host"
? provider === "claude"
Expand Down Expand Up @@ -4508,7 +4577,7 @@ describe("Account Pool plugin", () => {
try {
await movePoolToOtherAccount(fixture, provider);
attempts.splice(1);
now += 29 * 60 * 1_000;
now += 64 * 60 * 1_000;
await (await send("child-session", "parent-session")).text();
now += 2 * 60 * 1_000;
await (await send("parent-session", null)).text();
Expand Down Expand Up @@ -4684,11 +4753,11 @@ describe("Account Pool plugin", () => {
id: fixture.account.id,
});
expect(await send(fixture.key, "{}", {})).toBe(keyFor("sk-second"));
now += 29 * 60 * 1_000;
now += 64 * 60 * 1_000;
expect(await send(fixture.key, body)).toBe(keyFor("sk-first"));
now += 2 * 60 * 1_000;
expect(await send(fixture.key, body)).toBe(keyFor("sk-first"));
now += 31 * 60 * 1_000;
now += 66 * 60 * 1_000;
expect(await send(fixture.key, body)).toBe(keyFor("sk-second"));
} finally {
await held.body?.cancel();
Expand Down
Loading