194 lines
7.6 KiB
TypeScript
194 lines
7.6 KiB
TypeScript
import { test, expect } from "../../fixtures/test-base";
|
|
import { SessionPage } from "../../pages/session-page";
|
|
|
|
/**
|
|
* Bug: when a task is created via MCP `create_task_kandev` with
|
|
* `start_agent=false`, it has no session/environment. Selecting that task in
|
|
* the sidebar previously left the dockview layout corrupted (wrong widths on
|
|
* the central group) and did not update the breadcrumb to the new task. The
|
|
* corruption then persisted across subsequent task switches because the bad
|
|
* layout was saved back to per-session storage.
|
|
*
|
|
* This test reproduces the failing flow end-to-end and asserts:
|
|
* 1. Selecting the sessionless subtask updates the URL + breadcrumb.
|
|
* 2. The dockview layout remains healthy (no zero-width groups).
|
|
* 3. Switching back to the parent task keeps the layout healthy
|
|
* (regression on persisted-corruption).
|
|
*/
|
|
|
|
const SUBTASK_TITLE = "Sessionless MCP Subtask";
|
|
|
|
test.describe("Sessionless task switching", () => {
|
|
test("selecting an MCP-created sessionless subtask keeps layout healthy", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
test.setTimeout(90_000);
|
|
|
|
// Parent task agent script: create a subtask via MCP with start_agent=false.
|
|
const script = [
|
|
'e2e:thinking("Creating sessionless subtask...")',
|
|
"e2e:delay(100)",
|
|
`e2e:mcp:kandev:create_task_kandev({"parent_id":"self","title":"${SUBTASK_TITLE}","description":"Sessionless subtask for layout-recovery test","start_agent":false})`,
|
|
"e2e:delay(100)",
|
|
'e2e:message("Done.")',
|
|
].join("\n");
|
|
|
|
const parent = await apiClient.createTaskWithAgent(
|
|
seedData.workspaceId,
|
|
"Sessionless Parent Task",
|
|
seedData.agentProfileId,
|
|
{
|
|
description: script,
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
repository_ids: [seedData.repositoryId],
|
|
},
|
|
);
|
|
|
|
await testPage.goto(`/t/${parent.id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
|
|
// Wait for parent agent to settle so the layout has stabilised.
|
|
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
|
|
|
|
// Poll the API until the MCP-created subtask exists. We need its ID for URL
|
|
// assertions. The subtask is created by the agent asynchronously, so the
|
|
// idle indicator alone isn't a guarantee.
|
|
type TaskEntry = { id: string; title: string; primary_session_id?: string | null };
|
|
let subtask: TaskEntry | undefined;
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const all = await apiClient.listTasks(seedData.workspaceId);
|
|
subtask = all.tasks.find((t: TaskEntry) => t.title === SUBTASK_TITLE);
|
|
return subtask?.id ?? null;
|
|
},
|
|
{ timeout: 30_000, message: "Waiting for MCP-created subtask to appear" },
|
|
)
|
|
.toBeTruthy();
|
|
const subtaskId = subtask!.id;
|
|
// Sanity: the bug requires a sessionless task. start_agent=false implies no
|
|
// primary session was created on the backend.
|
|
expect(subtask!.primary_session_id ?? null).toBeNull();
|
|
|
|
// Baseline: layout is healthy on the parent.
|
|
await session.expectLayoutHealthy();
|
|
|
|
// Wait for the subtask to appear in the sidebar, then click it.
|
|
const subtaskRow = session.taskInSidebar(SUBTASK_TITLE);
|
|
await expect(subtaskRow).toBeVisible({ timeout: 10_000 });
|
|
await session.clickTaskInSidebar(SUBTASK_TITLE);
|
|
|
|
// URL should switch to the subtask within a reasonable time.
|
|
await expect(testPage).toHaveURL(new RegExp(`/t/${subtaskId}(?:\\?|$)`), {
|
|
timeout: 10_000,
|
|
});
|
|
|
|
// Breadcrumb (task title in the topbar) must reflect the new task.
|
|
const breadcrumb = testPage.locator('[aria-current="page"]');
|
|
await expect(breadcrumb).toHaveText(SUBTASK_TITLE, { timeout: 10_000 });
|
|
|
|
// Layout must remain healthy on the sessionless task.
|
|
await session.expectLayoutHealthy();
|
|
|
|
// Switch back to the parent task. The bug surfaces here: persisted bad
|
|
// layout state corrupts the central-group widths on this second switch.
|
|
await session.clickTaskInSidebar("Sessionless Parent Task");
|
|
await expect(testPage).toHaveURL(new RegExp(`/t/${parent.id}(?:\\?|$)`), {
|
|
timeout: 10_000,
|
|
});
|
|
await expect(breadcrumb).toHaveText("Sessionless Parent Task", { timeout: 10_000 });
|
|
await session.expectLayoutHealthy();
|
|
});
|
|
|
|
test("self-recovers when sessionStorage holds a corrupted layout for a known session", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
test.setTimeout(90_000);
|
|
|
|
// Two tasks with agents — A navigated first, then B, then back to A. The
|
|
// back-to-A switch goes through performEnvSwitch's slow path which
|
|
// historically applied the saved (potentially corrupt) blob verbatim.
|
|
const taskA = await apiClient.createTaskWithAgent(
|
|
seedData.workspaceId,
|
|
"Layout Recovery Task A",
|
|
seedData.agentProfileId,
|
|
{
|
|
description: "/e2e:simple-message",
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
repository_ids: [seedData.repositoryId],
|
|
},
|
|
);
|
|
const taskB = await apiClient.createTaskWithAgent(
|
|
seedData.workspaceId,
|
|
"Layout Recovery Task B",
|
|
seedData.agentProfileId,
|
|
{
|
|
description: "/e2e:simple-message",
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
repository_ids: [seedData.repositoryId],
|
|
},
|
|
);
|
|
|
|
await testPage.goto(`/t/${taskA.id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
// Don't wait for the agent to go idle: the seed workflow auto-starts a
|
|
// second session as soon as the first turn completes, so the chat
|
|
// composer cycles Preparing → idle (briefly) → Preparing within the
|
|
// polling window. The test only needs the dockview layout to be ready.
|
|
await session.waitForDockviewReady();
|
|
await session.expectLayoutHealthy();
|
|
|
|
// Resolve A's task environment id so we can target the env-keyed layout.
|
|
let sessionAEnvId: string | null = null;
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const { sessions } = await apiClient.listTaskSessions(taskA.id);
|
|
sessionAEnvId = sessions[0]?.task_environment_id ?? null;
|
|
return sessionAEnvId;
|
|
},
|
|
{ timeout: 15_000, message: "Waiting for task A environment id" },
|
|
)
|
|
.toBeTruthy();
|
|
|
|
// Switch to B, then back — this primes both layouts in sessionStorage.
|
|
await session.clickTaskInSidebar("Layout Recovery Task B");
|
|
await expect(testPage).toHaveURL(new RegExp(`/t/${taskB.id}(?:\\?|$)`), { timeout: 10_000 });
|
|
await session.expectLayoutHealthy();
|
|
|
|
// Inject a corrupt layout for A while we're focused on B.
|
|
await testPage.evaluate((envId) => {
|
|
const corrupt = {
|
|
grid: {
|
|
root: {
|
|
type: "leaf",
|
|
size: 800,
|
|
data: { id: "g1", views: ["chat"], activeView: "chat" },
|
|
},
|
|
height: 600,
|
|
width: 0, // <-- corruption
|
|
orientation: "HORIZONTAL",
|
|
},
|
|
panels: { chat: { id: "chat", contentComponent: "chat" } },
|
|
activeGroup: "g1",
|
|
};
|
|
window.sessionStorage.setItem(`kandev.dockview.env-layout.${envId}`, JSON.stringify(corrupt));
|
|
}, sessionAEnvId!);
|
|
|
|
// Switch back to A — performEnvSwitch should drop the corrupt blob
|
|
// and rebuild the default instead of applying the zero-width layout.
|
|
await session.clickTaskInSidebar("Layout Recovery Task A");
|
|
await expect(testPage).toHaveURL(new RegExp(`/t/${taskA.id}(?:\\?|$)`), { timeout: 10_000 });
|
|
await session.expectLayoutHealthy();
|
|
});
|
|
});
|