Files
kandev/apps/web/e2e/tests/task/plan-mode-followup.spec.ts
T
João Salavisa f74de1c642 fix: improve plan comment formatting to match code review style (#600)
* fix: improve plan comment formatting to match code review style

- Update formatPlanCommentsAsMarkdown to use code blocks and blockquotes
- Remove duplicate header in chat-input-area.tsx
- Update buildDocumentContext to use consistent formatting
- Fixes duplicate '### Plan Comments' headers and extra separators

* fix: add tests and fix multiline blockquote handling

- Add format.test.ts for formatPlanCommentsAsMarkdown and related functions
- Fix multiline comment text to properly prefix each line with >
- Addresses review feedback about missing tests and broken blockquotes

* fix: export and reuse toBlockquote helper to avoid duplication

* fix: update E2E test to match new plan comment format

The test was expecting 'Comment 1:' which was the old bullet-style format.
Now looks for 'Plan Comments' header instead.
2026-04-16 02:57:41 +01:00

191 lines
6.9 KiB
TypeScript

import { type Page } from "@playwright/test";
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";
// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------
/** MCP script that creates plan content the agent can interact with. */
const PLAN_SCRIPT = [
'e2e:thinking("Creating plan...")',
"e2e:delay(100)",
'e2e:mcp:kandev:create_task_plan_kandev({"task_id":"{task_id}","content":"## Plan\\n\\n1. Analyze requirements\\n2. Implement solution\\n3. Write tests","title":"Implementation Plan"})',
"e2e:delay(100)",
'e2e:message("Plan created.")',
].join("\n");
/** Create a task with agent, navigate to session, wait for idle. */
async function seedTaskAndWaitForIdle(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
title: string,
description = "/e2e:simple-message",
) {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
title,
seedData.agentProfileId,
{
description,
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 expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
return { session, taskId: task.id, sessionId: task.session_id! };
}
// ---------------------------------------------------------------------------
// Plan mode follow-up message tests
// ---------------------------------------------------------------------------
test.describe("Plan mode follow-up messages", () => {
test.describe.configure({ retries: 1 });
test("follow-up message in plan mode shows badge and stores plan_mode metadata", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(90_000);
const { session, sessionId } = await seedTaskAndWaitForIdle(
testPage,
apiClient,
seedData,
"Plan mode follow-up test",
);
// Enable plan mode after initial agent turn completes.
await session.togglePlanMode();
await expect(session.planModeInput()).toBeVisible({ timeout: 10_000 });
// Send a follow-up message in plan mode.
await session.sendMessage("Please add error handling to step 3 of the plan");
// The follow-up message should have the plan mode badge.
const planBadges = session.chat.getByText("Plan mode", { exact: true });
await expect(planBadges.last()).toBeVisible({ timeout: 15_000 });
// Wait for agent to complete and return to plan mode idle state.
await expect(session.planModeInput()).toBeVisible({ timeout: 30_000 });
// Verify the stored message has plan_mode metadata (confirms frontend sent plan_mode: true
// and backend stored it, which triggers InjectPlanMode in PromptTask).
const { messages } = await apiClient.listSessionMessages(sessionId);
const userFollowUp = messages.filter((m) => m.author_type === "user").pop();
expect(userFollowUp).toBeDefined();
expect(userFollowUp!.metadata?.plan_mode).toBe(true);
});
test("follow-up message without plan mode has no badge or plan_mode metadata", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(90_000);
const { session, sessionId } = await seedTaskAndWaitForIdle(
testPage,
apiClient,
seedData,
"No plan mode follow-up test",
);
// Send a follow-up without plan mode.
await session.sendMessage("implement the feature now");
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
// No plan mode badge.
const planBadges = session.chat.getByText("Plan mode", { exact: true });
await expect(planBadges).toHaveCount(0, { timeout: 5_000 });
// Verify no plan_mode metadata on the stored message.
const { messages } = await apiClient.listSessionMessages(sessionId);
const userFollowUp = messages.filter((m) => m.author_type === "user").pop();
expect(userFollowUp).toBeDefined();
expect(userFollowUp!.metadata?.plan_mode).toBeFalsy();
});
test("plan comment via Run button sends to agent with plan mode", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
// Create a task that produces plan content via MCP.
const { session, sessionId } = await seedTaskAndWaitForIdle(
testPage,
apiClient,
seedData,
"Plan comment Run test",
PLAN_SCRIPT,
);
// Toggle plan mode so the plan panel is visible with content.
await session.togglePlanMode();
await expect(session.planModeInput()).toBeVisible({ timeout: 10_000 });
await expect(session.planPanel.getByText("Analyze requirements", { exact: false })).toBeVisible(
{
timeout: 15_000,
},
);
// Select text in the plan editor to trigger the comment popover.
const editor = session.planPanel.locator(".ProseMirror");
await editor.click();
// Select all text with keyboard.
const modifier = process.platform === "darwin" ? "Meta" : "Control";
await testPage.keyboard.press(`${modifier}+a`);
// Open comment popover via Cmd+Shift+C.
await testPage.keyboard.press(`${modifier}+Shift+c`);
// Fill in the comment textarea.
const textarea = testPage.locator('textarea[placeholder="Add your comment or instruction..."]');
await expect(textarea).toBeVisible({ timeout: 5_000 });
await textarea.fill("Split step 2 into smaller sub-steps");
// Click the "Run" button (use exact match to avoid matching sidebar task button).
const runBtn = testPage.getByRole("button", { name: "Run", exact: true });
await expect(runBtn).toBeVisible({ timeout: 5_000 });
await runBtn.click();
// The comment should appear in the chat formatted as plan comment markdown.
await expect(session.chat.getByText("Plan Comments", { exact: false })).toBeVisible({
timeout: 15_000,
});
await expect(
session.chat.getByText("Split step 2 into smaller sub-steps", { exact: false }),
).toBeVisible({ timeout: 5_000 });
// Plan mode badge should be visible on the comment message.
const planBadges = session.chat.getByText("Plan mode", { exact: true });
await expect(planBadges.last()).toBeVisible({ timeout: 10_000 });
// Wait for agent to complete.
await expect(session.planModeInput()).toBeVisible({ timeout: 30_000 });
// Verify the comment message was stored with plan_mode metadata.
const { messages } = await apiClient.listSessionMessages(sessionId);
const commentMsg = messages
.filter((m) => m.author_type === "user")
.find((m) => m.content.includes("Split step 2"));
expect(commentMsg).toBeDefined();
expect(commentMsg!.metadata?.plan_mode).toBe(true);
});
});