Files
kandev/apps/web/components/task/executor-settings-button.test.tsx
T
José Almeida ce49d6dfec feat(web): show "Resuming session" feedback in popover during reconnect
The bottom Resume button on the chat panel was leaving users staring at
nothing for the 5-30s reconnect window: prepare-progress events are
intentionally suppressed on resume (the workspace is already there), so
the popover sat in the idle phase even while session.state was STARTING.

Synthesize a "resuming" phase from session state when no prepare events
are flowing, rendered as a spinner row in the popover with copy
"Resuming session — Reconnecting to the existing environment…". The
button icon also flips to the spinner so the toolbar advertises the
in-flight work without the user having to open the popover.

Extract the prepare-status reducer + section component into their own
file so the main popover module stays under the 600-line ESLint cap.
2026-05-05 23:14:28 +01:00

163 lines
5.8 KiB
TypeScript

import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { SessionPrepareState } from "@/lib/state/slices/session-runtime/types";
let mockPrepareState: SessionPrepareState | null = null;
let mockSessionState: string | null = null;
let mockEnv: { executor_type: string; sandbox_id?: string; container_id?: string } | null = null;
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
prepareProgress: {
bySessionId: mockPrepareState ? { [mockPrepareState.sessionId]: mockPrepareState } : {},
},
taskSessions: {
items: mockSessionState
? { [SESSION_ID]: { id: SESSION_ID, state: mockSessionState } }
: {},
},
}),
}));
vi.mock("@/lib/api/domains/task-environment-api", () => ({
fetchTaskEnvironmentLive: vi.fn().mockImplementation(async () => ({
environment: mockEnv ?? { executor_type: "" },
container: null,
})),
resetTaskEnvironment: vi.fn().mockResolvedValue({ success: true }),
}));
vi.mock("./task-reset-env-confirm-dialog", () => ({
TaskResetEnvConfirmDialog: () => null,
}));
import { ExecutorSettingsButton } from "./executor-settings-button";
const SESSION_ID = "session-1";
const TASK_ID = "task-1";
const STEP_CREATE_SANDBOX = "Creating cloud sandbox";
const PREPARE_STATUS_TESTID = "executor-prepare-status";
const SETTINGS_BUTTON_TESTID = "executor-settings-button";
describe("ExecutorSettingsButton", () => {
afterEach(() => {
cleanup();
mockPrepareState = null;
mockSessionState = null;
mockEnv = null;
});
it("shows the cloud icon when the executor type is sprites", async () => {
mockEnv = { executor_type: "sprites", sandbox_id: "kandev-abc" };
render(<ExecutorSettingsButton taskId={TASK_ID} sessionId={SESSION_ID} />);
// Wait a tick for the live fetch to resolve.
await Promise.resolve();
await Promise.resolve();
expect(await screen.findByTestId("executor-status-cloud-icon")).toBeTruthy();
});
it("shows the container icon for both docker variants", async () => {
mockEnv = { executor_type: "local_docker", container_id: "abcdef" };
render(<ExecutorSettingsButton taskId={TASK_ID} sessionId={SESSION_ID} />);
await Promise.resolve();
expect(await screen.findByTestId("executor-status-container-icon")).toBeTruthy();
});
it("swaps to a spinner while the prepare progress is preparing", async () => {
mockEnv = { executor_type: "sprites", sandbox_id: "kandev-abc" };
mockPrepareState = {
sessionId: SESSION_ID,
status: "preparing",
steps: [{ name: STEP_CREATE_SANDBOX, status: "running" }],
};
render(<ExecutorSettingsButton taskId={TASK_ID} sessionId={SESSION_ID} />);
expect(screen.getByTestId("executor-settings-button-spinner")).toBeTruthy();
expect(screen.queryByTestId("executor-status-cloud-icon")).toBeNull();
});
it("renders the preparing section with current step copy", async () => {
mockPrepareState = {
sessionId: SESSION_ID,
status: "preparing",
steps: [
{ name: STEP_CREATE_SANDBOX, status: "completed" },
{ name: "Uploading agent controller", status: "running" },
{ name: "Waiting for agent controller", status: "pending" },
],
};
render(<ExecutorSettingsButton taskId={TASK_ID} sessionId={SESSION_ID} />);
// Open the popover by clicking the trigger.
const trigger = screen.getByTestId(SETTINGS_BUTTON_TESTID);
trigger.click();
expect(await screen.findByTestId(PREPARE_STATUS_TESTID)).toHaveProperty(
"dataset.phase",
"preparing",
);
expect(screen.getByText(/Step 2 of 3: Uploading agent controller/)).toBeTruthy();
});
it("renders the fallback warning callout when the missing-sandbox notice is present", async () => {
mockPrepareState = {
sessionId: SESSION_ID,
status: "preparing",
steps: [
{
name: "Reconnecting cloud sandbox",
status: "skipped",
warning:
"Previous sandbox is no longer available — provisioning a fresh one for this branch.",
},
{ name: STEP_CREATE_SANDBOX, status: "running" },
],
};
render(<ExecutorSettingsButton taskId={TASK_ID} sessionId={SESSION_ID} />);
screen.getByTestId(SETTINGS_BUTTON_TESTID).click();
const status = await screen.findByTestId(PREPARE_STATUS_TESTID);
expect(status.dataset.phase).toBe("preparing_fallback");
expect(screen.getByTestId("executor-prepare-fallback-warning")).toBeTruthy();
});
it("renders the Resuming session row when session is STARTING with no prepare events", async () => {
mockSessionState = "STARTING";
render(<ExecutorSettingsButton taskId={TASK_ID} sessionId={SESSION_ID} />);
expect(screen.getByTestId("executor-settings-button-spinner")).toBeTruthy();
screen.getByTestId(SETTINGS_BUTTON_TESTID).click();
const status = await screen.findByTestId(PREPARE_STATUS_TESTID);
expect(status.dataset.phase).toBe("resuming");
expect(screen.getByText("Resuming session")).toBeTruthy();
expect(screen.getByText(/Reconnecting to the existing environment/)).toBeTruthy();
});
it("shows an Environment ready row once preparation completes", async () => {
mockPrepareState = {
sessionId: SESSION_ID,
status: "completed",
steps: [{ name: "Creating cloud sandbox", status: "completed" }],
durationMs: 12500,
};
render(<ExecutorSettingsButton taskId={TASK_ID} sessionId={SESSION_ID} />);
screen.getByTestId(SETTINGS_BUTTON_TESTID).click();
const status = await screen.findByTestId(PREPARE_STATUS_TESTID);
expect(status.dataset.phase).toBe("ready");
expect(screen.getByText(/Environment ready/)).toBeTruthy();
expect(screen.getByText(/13s/)).toBeTruthy();
});
});