From d35e601d42ed3c8dad3c7682b3c88cbb6452c3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= Date: Tue, 5 May 2026 23:05:22 +0100 Subject: [PATCH] =?UTF-8?q?test(docker):=20cover=20stop=20+=20resume=20?= =?UTF-8?q?=E2=86=92=20same=20container,=20branch=20preserved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Docker e2e test that exercises the user-visible stop-then-resume flow: launch session, stop it via WS session.stop, launch a fresh session on the same task, and assert the same task environment + container ID are reused and the kandev feature branch is preserved. Also nudge the executor popover padding: when the prepare-status section is rendered above the Sprites/Docker info block, the cumulative top+bottom paddings looked too generous around "Sprites sandbox" / "Local Docker". Tighten both sections vertically without affecting the horizontal rhythm or the bottom action row. --- .../task/executor-settings-button.tsx | 6 +- apps/web/e2e/helpers/api-client.ts | 9 +++ .../e2e/tests/docker/docker-launch.spec.ts | 73 +++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/apps/web/components/task/executor-settings-button.tsx b/apps/web/components/task/executor-settings-button.tsx index 6fa2ed192c..338e35b5ef 100644 --- a/apps/web/components/task/executor-settings-button.tsx +++ b/apps/web/components/task/executor-settings-button.tsx @@ -399,7 +399,7 @@ function PrepareStatusSection({ summary }: { summary: PrepareSummary }) {
@@ -428,7 +428,7 @@ function PrepareStatusSection({ summary }: { summary: PrepareSummary }) {
@@ -500,7 +500,7 @@ function EnvironmentInfo({ } return ( -
+
{formatExecutorType(env.executor_type)} diff --git a/apps/web/e2e/helpers/api-client.ts b/apps/web/e2e/helpers/api-client.ts index d5abe090c2..1b3cdfa25c 100644 --- a/apps/web/e2e/helpers/api-client.ts +++ b/apps/web/e2e/helpers/api-client.ts @@ -839,6 +839,15 @@ export class ApiClient { return this.wsRequest("session.launch", payload); } + /** Stop a running session via WS `session.stop` — same path the UI uses. */ + async stopSession(payload: { + session_id: string; + reason?: string; + force?: boolean; + }): Promise<{ success: boolean }> { + return this.wsRequest("session.stop", payload); + } + async getTaskEnvironment(taskId: string): Promise<{ id: string; task_id: string; diff --git a/apps/web/e2e/tests/docker/docker-launch.spec.ts b/apps/web/e2e/tests/docker/docker-launch.spec.ts index 390ea6fbb6..fdb5a0fb7e 100644 --- a/apps/web/e2e/tests/docker/docker-launch.spec.ts +++ b/apps/web/e2e/tests/docker/docker-launch.spec.ts @@ -565,6 +565,79 @@ test.describe("Docker executor — launch + reuse + recovery", () => { expect(launchedSession?.task_environment_id).toBe(before!.id); }); + test("stops the agent then resumes onto the same container without re-cloning", async ({ + apiClient, + seedData, + }) => { + test.setTimeout(240_000); + + const task = await apiClient.createTaskWithAgent( + seedData.workspaceId, + "Docker Stop Resume", + seedData.agentProfileId, + { + description: "/e2e:simple-message", + workflow_id: seedData.workflowId, + workflow_step_id: seedData.startStepId, + repository_ids: [seedData.repositoryId], + executor_profile_id: seedData.dockerExecutorProfileId, + }, + ); + await waitForLatestSessionDone(apiClient, task.id, 1, "Waiting for first Docker session"); + + const before = await apiClient.getTaskEnvironment(task.id); + expect(before?.container_id).toBeTruthy(); + const containerID = before!.container_id!; + const branchBefore = dockerCurrentBranch(containerID); + expect(branchBefore).toMatch(/^feature\/docker-stop-resume-[0-9a-f]{6}$/); + + const { sessions: sessionsBefore } = await apiClient.listTaskSessions(task.id); + const firstSession = sessionsBefore[0]; + expect(firstSession?.id).toBeTruthy(); + + // Stop via the same WS path the UI uses. + await apiClient.stopSession({ + session_id: firstSession!.id, + reason: "e2e stop", + force: false, + }); + await expect + .poll( + async () => { + const { sessions } = await apiClient.listTaskSessions(task.id); + return sessions.find((s) => s.id === firstSession!.id)?.state; + }, + { timeout: 30_000, message: "Waiting for first session to reach CANCELLED" }, + ) + .toBe("CANCELLED"); + + // Resume by launching a new session on the same task — the UI Resume + // button takes this path (it just creates a new session bound to the + // same task environment). + const launched = await apiClient.launchSession({ + task_id: task.id, + agent_profile_id: seedData.agentProfileId, + executor_profile_id: seedData.dockerExecutorProfileId, + workflow_step_id: seedData.startStepId, + prompt: "/e2e:simple-message", + }); + await waitForSessionDone( + apiClient, + task.id, + launched.session_id, + "Waiting for resumed Docker session", + ); + + const after = await apiClient.getTaskEnvironment(task.id); + expect(after?.id, "task environment is reused on resume").toBe(before!.id); + expect(after?.container_id, "container is reused on resume").toBe(containerID); + + // Container is still alive and on the same kandev feature branch — no + // re-clone, no re-checkout to base. + expect(dockerInspectExists(containerID)).toBe(true); + expect(dockerCurrentBranch(containerID), "feature branch is preserved").toBe(branchBefore); + }); + // FIXME: blocked on backend gap — DockerExecutor.reconnectToContainer constructs // its agentctl ControlClient with no auth token (the original handshake token // is held only in memory and lost across launches). Reconnect 401s, the