4228f92aef
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
200 lines
6.5 KiB
TypeScript
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();
|
|
});
|
|
});
|