208 lines
8.0 KiB
TypeScript
208 lines
8.0 KiB
TypeScript
import { test, expect } from "../../fixtures/test-base";
|
|
import type { SeedData } from "../../fixtures/test-base";
|
|
import type { ApiClient } from "../../helpers/api-client";
|
|
import { SessionPage } from "../../pages/session-page";
|
|
import { attachGatewayTrafficCapture } from "../../helpers/ws-traffic";
|
|
|
|
async function prepareMonitorTask(apiClient: ApiClient, seedData: SeedData, title: string) {
|
|
const task = await apiClient.createTask(seedData.workspaceId, title, {
|
|
agent_profile_id: seedData.agentProfileId,
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
repository_ids: [seedData.repositoryId],
|
|
});
|
|
const prepared = await apiClient.launchSession({
|
|
task_id: task.id,
|
|
agent_profile_id: seedData.agentProfileId,
|
|
executor_profile_id: seedData.worktreeExecutorProfileId,
|
|
workflow_step_id: seedData.startStepId,
|
|
prompt: "",
|
|
intent: "prepare",
|
|
launch_workspace: true,
|
|
});
|
|
|
|
await expect
|
|
.poll(async () => (await apiClient.getTaskEnvironment(task.id))?.status ?? null, {
|
|
timeout: 60_000,
|
|
message: "prepared Monitor task environment did not become ready",
|
|
})
|
|
.toBe("ready");
|
|
|
|
return { task, sessionId: prepared.session_id };
|
|
}
|
|
|
|
async function waitForMonitorSubscription(
|
|
capture: ReturnType<typeof attachGatewayTrafficCapture>,
|
|
sessionId: string,
|
|
) {
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
capture.frames.some(
|
|
(frame) =>
|
|
frame.direction === "sent" &&
|
|
frame.action === "session.subscribe" &&
|
|
frame.sessionId === sessionId,
|
|
),
|
|
{ timeout: 10_000, message: "Monitor page must subscribe before the script starts" },
|
|
)
|
|
.toBe(true);
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
capture.frames.some(
|
|
(frame) =>
|
|
frame.direction === "received" &&
|
|
frame.action === "session.subscribe" &&
|
|
frame.sessionId === sessionId,
|
|
),
|
|
{ timeout: 30_000, message: "Monitor page did not acknowledge its session subscription" },
|
|
)
|
|
.toBe(true);
|
|
}
|
|
|
|
// 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(240_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:delay(500)",
|
|
'e2e:monitor_end("task-1")',
|
|
'e2e:message("watching done")',
|
|
].join("\n");
|
|
|
|
const { task, sessionId } = await prepareMonitorTask(apiClient, seedData, "Monitor watching");
|
|
|
|
const capture = attachGatewayTrafficCapture(testPage);
|
|
await testPage.goto(`/t/${task.id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
await session.waitForChatIdle({ timeout: 30_000 });
|
|
await waitForMonitorSubscription(capture, sessionId);
|
|
capture.frames.length = 0;
|
|
// The browser send path records the first user turn before launching the
|
|
// agent, so fast Monitor notifications have an active turn to update.
|
|
await session.sendMessageViaButton(script);
|
|
|
|
// The dedicated Monitor card renders, not the generic tool_call row.
|
|
const monitorCard = session.activeChat().locator('[data-testid="monitor-card"]');
|
|
await expect(monitorCard).toHaveCount(1);
|
|
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", { timeout: 30_000 });
|
|
await expect(monitorCard.getByTestId("monitor-status-pill")).toContainText(/ended/);
|
|
|
|
// Each event body landed in the recent-events tail.
|
|
const eventList = session.activeChat().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.activeChat()).not.toContainText("<task-notification>");
|
|
await expect(session.activeChat()).not.toContainText("Human: <task");
|
|
});
|
|
|
|
test("singular event count uses 'event' not 'events'", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
test.setTimeout(240_000);
|
|
|
|
const script = [
|
|
'e2e:monitor_start("task-1", "tail -f log")',
|
|
"e2e:delay(500)",
|
|
'e2e:monitor_event("task-1", "first and only line")',
|
|
"e2e:delay(500)",
|
|
'e2e:monitor_end("task-1")',
|
|
'e2e:message("done")',
|
|
].join("\n");
|
|
|
|
const { task, sessionId } = await prepareMonitorTask(apiClient, seedData, "Monitor singular");
|
|
|
|
const capture = attachGatewayTrafficCapture(testPage);
|
|
await testPage.goto(`/t/${task.id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
await session.waitForChatIdle({ timeout: 30_000 });
|
|
await waitForMonitorSubscription(capture, sessionId);
|
|
await session.sendMessageViaButton(script);
|
|
const card = session.activeChat().locator('[data-testid="monitor-card"]');
|
|
await expect(card).toHaveCount(1);
|
|
await expect(card).toContainText("1 event", { timeout: 30_000 });
|
|
await expect(card).not.toContainText("1 events");
|
|
});
|
|
|
|
test("page reload preserves the monitor card and recent events", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
test.setTimeout(240_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:delay(500)",
|
|
'e2e:monitor_end("task-1")',
|
|
'e2e:message("done")',
|
|
].join("\n");
|
|
|
|
const { task, sessionId } = await prepareMonitorTask(apiClient, seedData, "Monitor reload");
|
|
|
|
const capture = attachGatewayTrafficCapture(testPage);
|
|
await testPage.goto(`/t/${task.id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
await session.waitForChatIdle({ timeout: 30_000 });
|
|
await waitForMonitorSubscription(capture, sessionId);
|
|
await session.sendMessageViaButton(script);
|
|
const cards = session.activeChat().locator('[data-testid="monitor-card"]');
|
|
await expect(cards).toHaveCount(1);
|
|
await expect(cards).toContainText("2 events", { timeout: 30_000 });
|
|
|
|
// 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.activeChat().locator('[data-testid="monitor-card"]');
|
|
await expect(card).toHaveCount(1);
|
|
await expect(card).toBeVisible();
|
|
await expect(card).toContainText("wait for ci");
|
|
await expect(card).toContainText("2 events");
|
|
await expect(session.activeChat().locator('[data-testid="monitor-event"]')).toHaveCount(2);
|
|
});
|
|
});
|