fix(core,web): retry transport and quota LLM errors, disable dead sessions on auth failure (#82)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c369e089a7
commit
3d3495528c
54 files changed
+2278
-161
No files matched your search
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Integration tests for POST /api/sessions/:id/retry-now ("retry now" on the reconnect
|
||||
* countdown — skips the in-progress backoff wait):
|
||||
* - 200 {skipped:true} while the runtime reports a wait was skipped;
|
||||
* - 200 {skipped:false} as a benign no-op when the session is idle or no wait is in
|
||||
* progress (timing races must not surface as errors);
|
||||
* - 404 for foreign/unknown sessions (via the shared resolveSession lookup).
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { approvalDecision, assistantText, toolCall, userText } from "@prismshadow/penguin-core";
|
||||
import type { ApproveFn, OmniMessage } from "@prismshadow/penguin-core";
|
||||
import type { RetryNowResponse } from "../src/api/types.js";
|
||||
import type { SessionRow } from "../src/db/repos/sessions.js";
|
||||
import type { RuntimeSession } from "../src/runtime/session-manager.js";
|
||||
import { apiClient, createTestApp, provisionUser, waitFor } from "./helpers.js";
|
||||
import type { TestApp } from "./helpers.js";
|
||||
|
||||
const SID = "session-2026-07-27-10-00-00-ccdd0002";
|
||||
|
||||
/** Fake Session that parks on one approval (keeps the Task running) and records skip calls. */
|
||||
function backoffFakeSession(sessionId: string, skips: number[]): RuntimeSession {
|
||||
return {
|
||||
sessionId,
|
||||
toolPermission: () => "rw",
|
||||
generateTitle: async () => ({ title: null, usage: null }),
|
||||
compactability: () => "ok" as const,
|
||||
steer: () => false,
|
||||
skipReconnectWait: () => {
|
||||
skips.push(Date.now());
|
||||
return true; // pretend a reconnect wait was in progress and got skipped
|
||||
},
|
||||
async *run(_input: OmniMessage[], opts: { approve: ApproveFn; signal: AbortSignal }) {
|
||||
const tc = toolCall({ name: "exec_command", arguments: "{}", toolCallId: "tc-retry" });
|
||||
yield tc;
|
||||
const decision = await opts.approve(tc);
|
||||
yield approvalDecision(decision, "tc-retry");
|
||||
yield assistantText("done");
|
||||
},
|
||||
async *compact() {},
|
||||
};
|
||||
}
|
||||
|
||||
describe("retry-now route", () => {
|
||||
let t: TestApp;
|
||||
let api: ReturnType<typeof apiClient>;
|
||||
let outsider: ReturnType<typeof apiClient>;
|
||||
let skips: number[];
|
||||
|
||||
beforeEach(async () => {
|
||||
t = await createTestApp();
|
||||
const { cookie } = await provisionUser(t.app, "retrier");
|
||||
const other = await provisionUser(t.app, "outsider_r");
|
||||
api = apiClient(t.app, cookie);
|
||||
outsider = apiClient(t.app, other.cookie);
|
||||
const row: SessionRow = {
|
||||
sessionId: SID,
|
||||
projectId: "retrier-default_project",
|
||||
agentId: "default_agent",
|
||||
modelId: "m1",
|
||||
provider: "custom",
|
||||
workspace: "/tmp/w",
|
||||
approvalMode: "always-ask",
|
||||
title: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
t.deps.sessionsRepo.insert(row);
|
||||
skips = [];
|
||||
t.deps.manager.adopt(row, backoffFakeSession(SID, skips));
|
||||
});
|
||||
afterEach(async () => {
|
||||
await t.cleanup();
|
||||
});
|
||||
|
||||
it("idle → benign 200 {skipped:false}; the runtime is never poked", async () => {
|
||||
const res = await api.post(`/api/sessions/${SID}/retry-now`, {});
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()) as RetryNowResponse).toEqual({ skipped: false });
|
||||
expect(skips).toEqual([]);
|
||||
});
|
||||
|
||||
it("running (parked in a wait) → 200 {skipped:true}, the skip reaches the runtime", async () => {
|
||||
await t.deps.manager.startTask(SID, [userText("go")]);
|
||||
await waitFor(() => t.deps.manager.pendingApprovalCount(SID) === 1);
|
||||
|
||||
const res = await api.post(`/api/sessions/${SID}/retry-now`, {});
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()) as RetryNowResponse).toEqual({ skipped: true });
|
||||
expect(skips).toHaveLength(1);
|
||||
|
||||
t.deps.manager.decideApproval(SID, "tc-retry", "allow");
|
||||
await waitFor(() => t.deps.manager.statusOf(SID) === "idle");
|
||||
});
|
||||
|
||||
it("foreign and unknown sessions → 404 (same auth semantics as the other session routes)", async () => {
|
||||
expect((await outsider.post(`/api/sessions/${SID}/retry-now`, {})).status).toBe(404);
|
||||
expect((await api.post(`/api/sessions/session-ghost/retry-now`, {})).status).toBe(404);
|
||||
expect(skips).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user