Files
kandev/apps/web/e2e/tests/task/task-interrupted-icon.spec.ts
T
2026-09-21 14:07:49 +01:00

108 lines
4.4 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
// Interrupted-task indicator: when Kandev dies while a task's session is
// mid-turn, startup reconciliation marks the task with the `interrupted_at`
// metadata key and the task list shows a warning triangle until the task is
// resumed. Seeding `interrupted_at` through the public task-metadata surface
// is the deterministic stand-in for a real crash; the icon itself must render
// from that marker alone.
test.describe("Task list — interrupted-task icon", () => {
test("shows the warning triangle only for marked tasks", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(90_000);
// Anchor task with an agent so we can open a session page and view the
// sidebar in its normal SSR-hydrated context.
await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Anchor Session",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
// The post-restart shape: a marked task reconciled to REVIEW with a
// WAITING_FOR_INPUT session.
const interrupted = await apiClient.createTask(seedData.workspaceId, "Interrupted Fixture", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
metadata: { interrupted_at: "2026-08-02T10:00:00Z" },
});
await apiClient.updateTaskState(interrupted.id, "REVIEW");
const plain = await apiClient.createTask(seedData.workspaceId, "Plain Fixture", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
await apiClient.updateTaskState(plain.id, "REVIEW");
// A terminal marked task must keep its done affordance, never the red icon.
const terminal = await apiClient.createTask(seedData.workspaceId, "Terminal Fixture", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
metadata: { interrupted_at: "2026-08-02T10:00:00Z" },
});
await apiClient.updateTaskState(terminal.id, "COMPLETED");
const kanban = new KanbanPage(testPage);
const workflowSnapshot = testPage.waitForResponse(
(response) =>
response.request().method() === "GET" &&
response.url().includes(`/api/v1/workflows/${seedData.workflowId}/snapshot`) &&
response.ok(),
);
await kanban.goto();
await workflowSnapshot;
const interruptedCard = kanban.taskCard(interrupted.id);
await expect(interruptedCard).toBeVisible();
const cardIcon = interruptedCard.getByTestId("task-state-interrupted");
await expect(cardIcon).toBeVisible();
await expect(cardIcon).toHaveClass(/tabler-icon-alert-triangle/);
await expect(cardIcon).toHaveClass(/text-yellow-500/);
const anchorCard = kanban.taskCardByTitle("Anchor Session");
await expect(anchorCard).toBeVisible();
await anchorCard.click();
await expect(testPage).toHaveURL(/\/t\//);
const session = new SessionPage(testPage);
await session.waitForLoad();
const interruptedRow = session.sidebarTaskItem("Interrupted Fixture");
await expect(interruptedRow).toBeVisible();
await expect(interruptedRow.getByTestId("task-state-interrupted")).toBeVisible();
const plainRow = session.sidebarTaskItem("Plain Fixture");
await expect(plainRow).toBeVisible();
await expect(plainRow.getByTestId("task-state-interrupted")).toHaveCount(0);
const terminalRow = session.sidebarTaskItem("Terminal Fixture");
await expect(terminalRow).toBeVisible();
await expect(terminalRow.getByTestId("task-state-interrupted")).toHaveCount(0);
// Reload: the marker must survive SSR hydration from the boot payload.
await testPage.reload();
await session.waitForLoad();
await expect(session.sidebarTaskItem("Interrupted Fixture")).toBeVisible();
await expect(
session.sidebarTaskItem("Interrupted Fixture").getByTestId("task-state-interrupted"),
).toBeVisible();
await expect(
session.sidebarTaskItem("Plain Fixture").getByTestId("task-state-interrupted"),
).toHaveCount(0);
});
});