/** * 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; let outsider: ReturnType; 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([]); }); });