Files
kandev/apps/web/e2e/tests/chat/monitor.spec.ts
T

163 lines
5.8 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { SessionPage } from "../../pages/session-page";
// All Monitor scenarios drive the kandev backend with the mock-agent's new
// e2e:monitor_* directives, which reproduce claude-agent-acp's wire format
// without depending on the real Claude Code SDK. The kandev ACP adapter
// recognizes the registration banner, parses task-notification envelopes,
// strips the model's "Human:" echoes, and tracks live monitor state — those
// behaviours are unit-tested in the backend; this file asserts the full
// pipeline lands the right thing in the chat UI.
test.describe("Claude-acp Monitor tool", () => {
test("renders watching card, accumulates events, hides envelope text", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(60_000);
// Three monitor events fire over ~3s. The agent then ends the monitor and
// produces a final assistant message so the turn completes deterministically.
const script = [
'e2e:monitor_start("task-1", "gh pr checks --watch")',
"e2e:delay(200)",
'e2e:monitor_event("task-1", "queued: lint")',
"e2e:delay(200)",
'e2e:monitor_event("task-1", "in_progress: lint")',
"e2e:delay(200)",
'e2e:monitor_event("task-1", "success: lint")',
'e2e:monitor_end("task-1")',
'e2e:message("watching done")',
].join("\n");
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Monitor watching",
seedData.agentProfileId,
{
description: script,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle({ timeout: 30_000 });
// The dedicated Monitor card renders, not the generic tool_call row.
const monitorCard = session.chat.locator('[data-testid="monitor-card"]').first();
await expect(monitorCard).toBeVisible();
await expect(monitorCard).toContainText("gh pr checks --watch");
// Event count badge surfaces all three events. Status pill flipped to
// "ended" because the script issued monitor_end and the parent turn
// completed.
await expect(monitorCard).toContainText("3 events");
await expect(session.chat.locator('[data-testid="monitor-status-pill"]').first()).toContainText(
/ended/,
);
// Each event body landed in the recent-events tail.
const eventList = session.chat.locator('[data-testid="monitor-event"]');
await expect(eventList).toHaveCount(3);
await expect(eventList.nth(0)).toContainText("queued: lint");
await expect(eventList.nth(2)).toContainText("success: lint");
// Critical: the model's "Human: <task-notification>" echo must NOT appear
// anywhere in the chat. The adapter strips matched envelopes from the
// assistant text and drops orphan "Human:" prefixes entirely.
await expect(session.chat).not.toContainText("<task-notification>");
await expect(session.chat).not.toContainText("Human: <task");
});
test("singular event count uses 'event' not 'events'", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(45_000);
const script = [
'e2e:monitor_start("task-1", "tail -f log")',
"e2e:delay(100)",
'e2e:monitor_event("task-1", "first and only line")',
'e2e:monitor_end("task-1")',
'e2e:message("done")',
].join("\n");
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Monitor singular",
seedData.agentProfileId,
{
description: script,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle({ timeout: 30_000 });
const card = session.chat.locator('[data-testid="monitor-card"]').first();
await expect(card).toContainText("1 event");
await expect(card).not.toContainText("1 events");
});
test("page reload preserves the monitor card and recent events", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(60_000);
const script = [
'e2e:monitor_start("task-1", "wait for ci")',
"e2e:delay(150)",
'e2e:monitor_event("task-1", "step-1")',
"e2e:delay(150)",
'e2e:monitor_event("task-1", "step-2")',
'e2e:monitor_end("task-1")',
'e2e:message("done")',
].join("\n");
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Monitor reload",
seedData.agentProfileId,
{
description: script,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle({ timeout: 30_000 });
await expect(session.chat.locator('[data-testid="monitor-card"]').first()).toContainText(
"2 events",
);
// Reload — SSR + Zustand hydration must reconstitute the card from DB.
await testPage.reload();
await session.waitForLoad();
await session.waitForChatIdle({ timeout: 30_000 });
const card = session.chat.locator('[data-testid="monitor-card"]').first();
await expect(card).toBeVisible();
await expect(card).toContainText("wait for ci");
await expect(card).toContainText("2 events");
await expect(session.chat.locator('[data-testid="monitor-event"]')).toHaveCount(2);
});
});