3d3495528c
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
/**
|
|
* 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([]);
|
|
});
|
|
});
|