Files
kandev/apps/web/e2e/tests/session/session-resume-cli-fallback.spec.ts
T

105 lines
4.3 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
/**
* Regression: a CLI passthrough agent that fast-fails on resume (e.g. real
* Claude CLI exits "No conversation found to continue") should auto-recover
* by relaunching once with a fresh command (no resume flag) instead of
* leaving the user stuck with a red banner.
*
* Driven via the mock-agent's --fail-on-resume flag: when invoked with
* -c / --resume the mock prints the canonical error and exits 1; otherwise
* it boots normally. The lifecycle manager's resume-fallback path should
* detect the fast-fail and relaunch fresh.
*/
async function createTUIProfileWithFailOnResume(apiClient: ApiClient, name: string) {
const { agents } = await apiClient.listAgents();
// Resolve the mock agent by identity rather than position — this spec
// depends on mock-agent-only behaviour (--fail-on-resume + the "Mock Agent"
// header), so picking agents[0] would silently exercise the wrong agent if
// listAgents() ever changes order.
const mockAgent = agents.find((a) => a.name === "mock-agent");
if (!mockAgent) {
throw new Error(
`mock-agent not found in listAgents() (got ${agents.map((a) => `${a.id}=${a.name}`).join(", ")})`,
);
}
return apiClient.createAgentProfile(mockAgent.id, name, {
model: "mock-fast",
cli_passthrough: true,
cli_flags: [{ description: "fail on resume", flag: "--fail-on-resume", enabled: true }],
});
}
test.describe("Session resume — CLI fallback after fast-fail", () => {
test.describe.configure({ retries: 1 });
test("relaunches without resume flag when CLI exits fast on -c", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
test.setTimeout(120_000);
const profile = await createTUIProfileWithFailOnResume(apiClient, "TUI Resume Fallback");
await apiClient.createTaskWithAgent(
seedData.workspaceId,
"TUI Resume Fallback Task",
profile.id,
{
description: "hello from resume fallback test",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
const kanban = new KanbanPage(testPage);
await kanban.goto();
const card = kanban.taskCardByTitle("TUI Resume Fallback Task");
await expect(card).toBeVisible({ timeout: 15_000 });
await card.click();
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
const session = new SessionPage(testPage);
await session.waitForPassthroughLoad();
await session.waitForPassthroughLoaded();
// Initial fresh launch: --fail-on-resume is present but no resume flag,
// so the mock-agent boots normally and renders the standard header.
await session.expectPassthroughHasText("Mock Agent");
// Wait for the workflow step to advance so the session is in
// WAITING_FOR_INPUT — the precondition for resume on next launch.
await expect(session.stepperStep("Review")).toHaveAttribute("aria-current", "step", {
timeout: 30_000,
});
// Restart the backend, then reload the page to trigger a session resume.
// The first launch attempt will pass -c → mock prints "No conversation
// found to continue" and exits 1 → backend clears the resume intent and
// the next launch (whether triggered inline by the fallback goroutine or
// by the next WS reconnect) goes through the fresh-launch path.
await backend.restart();
await testPage.reload();
await session.waitForPassthroughLoad();
// Skip waitForPassthroughLoaded — with --fail-on-resume the first PTY
// exits before the terminal WS settles, so the loading overlay can stay
// up briefly while the backend pivots to the fresh launch. Poll on the
// fresh-launch header directly: it only renders when PTY2 is alive and
// the WS is connected, which is a stronger signal than the loading flag.
await session.expectPassthroughHasText("Mock Agent", 60_000);
// The (RESUMED) header is reserved for runs where the resume flag was
// accepted. Asserting its absence confirms the second launch dropped -c.
await session.expectPassthroughNotHasText("RESUMED");
});
});