Files
kandev/apps/web/e2e/tests/session/executor-reuse.spec.ts
T
José Almeida 4228f92aef feat(web): add executor settings popover with reset environment action
A new button on the task top bar opens a popover showing the active
TaskEnvironment — executor type, status, container/sprite/worktree
identifiers, and a copy-ready 'docker exec' command for local Docker.
The popover hosts the destructive Reset Environment action so users have
one obvious place to inspect and manage the environment without
cluttering the New Session dialog.

The reset confirmation dialog requires explicit acknowledgement that
uncommitted changes will be lost. When the environment has a host-side
worktree it offers an opt-in 'push branch before resetting' checkbox so
committed work can be pushed first; the backend aborts the reset on
push failure.

- task-environment-api.ts: fetchTaskEnvironment + resetTaskEnvironment
- executor-settings-button.tsx: popover trigger + identity panel
- task-reset-env-confirm-dialog.tsx: destructive confirm with push opt-in
- task-top-bar.tsx: wire the new button into the right-side controls
- e2e: replace fresh-env-toggle test with reset-from-popover flow
2026-05-04 21:08:51 +01:00

200 lines
6.5 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
const DONE_STATES = ["COMPLETED", "WAITING_FOR_INPUT"];
/**
* Tests for executor environment reuse across sessions.
* Verifies that task environments persist across sessions and that
* the API response includes container_id/sandbox_id fields.
*
* Note: E2E tests run with KANDEV_DOCKER_ENABLED=false and KANDEV_MOCK_AGENT=only,
* so Docker container reuse cannot be tested end-to-end here.
* Docker reconnect logic is covered by Go unit tests.
*/
test.describe("Executor reuse", () => {
test("task environment API response includes executor_type field", async ({
apiClient,
seedData,
}) => {
// Create task with session, wait for completion
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Env Fields Task",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return DONE_STATES.includes(sessions[0]?.state ?? "");
},
{ timeout: 30_000, message: "Waiting for session to finish" },
)
.toBe(true);
// Get environment and verify response shape
const env = await apiClient.getTaskEnvironment(task.id);
expect(env).not.toBeNull();
expect(env!.task_id).toBe(task.id);
expect(env!.status).toBe("ready");
// executor_type should be present (standalone for mock agent)
expect(env!.executor_type).toBeDefined();
});
test("second session reuses same task environment by default", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
// 1. Create task with first session
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Reuse Env Task",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
// 2. Wait for first session to complete
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return DONE_STATES.includes(sessions[0]?.state ?? "");
},
{ timeout: 30_000, message: "Waiting for first session to finish" },
)
.toBe(true);
// 3. Capture environment
const envBefore = await apiClient.getTaskEnvironment(task.id);
expect(envBefore).not.toBeNull();
// 4. Navigate to task and create second session via dialog
const kanban = new KanbanPage(testPage);
await kanban.goto();
const card = kanban.taskCardByTitle("Reuse Env Task");
await expect(card).toBeVisible({ timeout: 10_000 });
await card.click();
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
const session = new SessionPage(testPage);
await session.waitForLoad();
await expect(session.chat.getByText("simple mock response", { exact: false })).toBeVisible({
timeout: 15_000,
});
// 5. Open new session dialog and submit
await session.openNewSessionDialog();
await expect(session.newSessionDialog()).toBeVisible({ timeout: 5_000 });
await session.newSessionPromptInput().fill("/e2e:simple-message");
await session.newSessionStartButton().click();
await expect(session.newSessionDialog()).not.toBeVisible({ timeout: 10_000 });
// 6. Wait for second session
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.length;
},
{ timeout: 30_000, message: "Waiting for second session to appear" },
)
.toBe(2);
// 7. Verify same environment ID (default reuse behavior)
const envAfter = await apiClient.getTaskEnvironment(task.id);
expect(envAfter).not.toBeNull();
expect(envAfter!.id).toBe(envBefore!.id);
// 8. Both sessions reference the same environment
const { sessions: allSessions } = await apiClient.listTaskSessions(task.id);
for (const s of allSessions) {
expect(s.task_environment_id).toBe(envBefore!.id);
}
});
test("reset environment from executor settings popover removes the environment", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(90_000);
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Reset Env Task",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return DONE_STATES.includes(sessions[0]?.state ?? "");
},
{ timeout: 30_000, message: "Waiting for session to finish" },
)
.toBe(true);
const envBefore = await apiClient.getTaskEnvironment(task.id);
expect(envBefore).not.toBeNull();
// Navigate to task
const kanban = new KanbanPage(testPage);
await kanban.goto();
const card = kanban.taskCardByTitle("Reset Env Task");
await expect(card).toBeVisible({ timeout: 10_000 });
await card.click();
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
const session = new SessionPage(testPage);
await session.waitForLoad();
// Open popover
await testPage.getByTestId("executor-settings-button").click();
const popover = testPage.getByTestId("executor-settings-popover");
await expect(popover).toBeVisible({ timeout: 5_000 });
// Click Reset → confirmation dialog
await testPage.getByTestId("executor-settings-reset").click();
const ackCheckbox = testPage.getByRole("checkbox").last();
await ackCheckbox.click();
await testPage.getByTestId("reset-env-confirm").click();
// Poll environment until gone
await expect
.poll(
async () => {
const env = await apiClient.getTaskEnvironment(task.id);
return env;
},
{ timeout: 15_000, message: "Waiting for environment to be reset" },
)
.toBeNull();
});
});