import { expect, test as base } from "../support/fixtures"; import { awaitAssistantMessage } from "../support/helpers/agent-stream"; import { expectChatHistoryAttachment, expectInFlightForkAvailable, expectLiveAssistantText, forkInFlightTurnToNewTab, forkMostRecentAssistantTurnToNewTab, forkMostRecentAssistantTurnToNewWorkspace, observeForkAttachment, } from "../support/helpers/assistant-fork"; import { expectComposerVisible, submitMessage } from "../support/helpers/composer"; import { getE2EDaemonPort } from "../support/helpers/daemon-port"; import { openAgentRoute, seedMockAgentWorkspace, type MockAgentOptions, type MockAgentWorkspace, } from "../support/helpers/mock-agent"; import { getServerId } from "../support/helpers/server-id"; import { seedSavedSettingsHosts } from "../support/helpers/settings"; import { submitNewWorkspaceEmpty } from "../support/helpers/new-workspace"; const test = base.extend<{ seedForkWorkspace: (options: MockAgentOptions) => Promise; }>({ seedForkWorkspace: async ({ browserName: _browserName }, provide) => { const sessions: MockAgentWorkspace[] = []; await provide(async (options) => { const session = await seedMockAgentWorkspace(options); sessions.push(session); return session; }); await Promise.allSettled(sessions.map((session) => session.cleanup())); }, }); test.describe("Assistant fork menu", () => { test.describe.configure({ timeout: 180_000 }); test("forks a failed assistant turn that has no provider message id", async ({ page, seedForkWorkspace, }) => { const session = await seedForkWorkspace({ repoPrefix: "assistant-fork-failed-turn-", title: "Assistant fork failed turn", model: "ten-second-stream", }); await openAgentRoute(page, session); await expectComposerVisible(page); await submitMessage(page, "Emit a synthetic turn failure."); await expect(page.getByText("[System Error] Requested mock provider failure")).toBeVisible({ timeout: 30_000, }); await forkMostRecentAssistantTurnToNewTab(page); await expectChatHistoryAttachment(page); }); test("forks a streaming assistant turn without interrupting it", async ({ page, seedForkWorkspace, }) => { const visibleBeforeFork = "where the auto-scroll logic actually lives"; const visibleAfterFork = "the first useful step is to read the relevant files"; const sourceAgentTitle = "Assistant fork in flight"; const forkAttachment = observeForkAttachment(page); const session = await seedForkWorkspace({ repoPrefix: "assistant-fork-in-flight-", title: sourceAgentTitle, model: "thirty-minute-stream", }); await openAgentRoute(page, session); await expectComposerVisible(page); await submitMessage(page, "Walk me through the scroll anchor behavior."); await expectInFlightForkAvailable(page); await expectLiveAssistantText(page, visibleBeforeFork); await forkInFlightTurnToNewTab(page); await expectChatHistoryAttachment(page); expect(await forkAttachment.waitForText()).toContain(visibleBeforeFork); await page.getByRole("button", { name: sourceAgentTitle }).click(); await expectLiveAssistantText(page, visibleAfterFork); }); test("focuses a forked assistant turn in a new workspace draft tab", async ({ page, seedForkWorkspace, }) => { const session = await seedForkWorkspace({ repoPrefix: "assistant-fork-focused-tab-", title: "Assistant fork focused tab", initialPrompt: "emit 1 coalesced agent stream updates for initial assistant fork turn.", model: "ten-second-stream", }); await openAgentRoute(page, session); await expectComposerVisible(page); await awaitAssistantMessage(page); await session.client.waitForFinish(session.agentId, 45_000); await submitMessage(page, "emit 1 coalesced agent stream updates while this tab is visible."); await session.client.waitForFinish(session.agentId, 45_000); await awaitAssistantMessage(page); const agentTab = page.getByTestId(`workspace-tab-agent_${session.agentId}`); await expect(agentTab).toHaveAttribute("aria-selected", "true"); await forkMostRecentAssistantTurnToNewTab(page); const selectedTab = page .getByTestId("workspace-tabs-row") .getByRole("button") .and(page.locator('[aria-selected="true"]')); await expect(selectedTab).toHaveAttribute("data-testid", /^workspace-tab-draft_/, { timeout: 30_000, }); await expect(agentTab).toHaveAttribute("aria-selected", "false"); await expectChatHistoryAttachment(page); }); test("keeps the fork attachment after submitting an existing-workspace draft tab", async ({ page, seedForkWorkspace, }) => { const session = await seedForkWorkspace({ repoPrefix: "assistant-fork-tab-submit-", title: "Assistant fork tab submit", initialPrompt: "emit 1 coalesced agent stream updates for assistant fork tab submit.", model: "ten-second-stream", }); await openAgentRoute(page, session); await expectComposerVisible(page); await awaitAssistantMessage(page); await session.client.waitForFinish(session.agentId, 45_000); await forkMostRecentAssistantTurnToNewTab(page); await expectChatHistoryAttachment(page); await submitMessage(page, ""); const userMessage = page.getByTestId("user-message").filter({ hasText: "Chat history" }).last(); await expect(userMessage).toBeVisible({ timeout: 30_000 }); await expect(userMessage).not.toContainText("Source agent:"); }); test("forks an assistant turn into New Workspace and keeps the attachment across host changes", async ({ page, seedForkWorkspace, }) => { await seedSavedSettingsHosts(page, [ { serverId: getServerId(), label: "localhost", endpoint: `127.0.0.1:${getE2EDaemonPort()}`, }, { serverId: "secondary-assistant-fork-host", label: "Secondary host", // The host does not need to be reachable; this pins that the draft-scoped // attachment survives changing the selected target host. endpoint: "127.0.0.1:9", }, ]); const session = await seedForkWorkspace({ repoPrefix: "assistant-fork-workspace-", title: "Assistant fork workspace", initialPrompt: "emit 1 coalesced agent stream updates for assistant fork new workspace.", model: "ten-second-stream", }); await openAgentRoute(page, session); await expectComposerVisible(page); await awaitAssistantMessage(page); await session.client.waitForFinish(session.agentId, 45_000); await forkMostRecentAssistantTurnToNewWorkspace(page); await expect(page).toHaveURL(/\/new\?.*draftId=/, { timeout: 30_000 }); await expectChatHistoryAttachment(page); await page.getByTestId("host-picker-trigger").click(); await page .getByTestId("new-workspace-host-picker-option-secondary-assistant-fork-host") .click(); await expectChatHistoryAttachment(page); }); test("keeps the fork attachment after the new agent receives its user message", async ({ page, seedForkWorkspace, }) => { const session = await seedForkWorkspace({ repoPrefix: "assistant-fork-submit-", title: "Assistant fork submit", initialPrompt: "emit 1 coalesced agent stream updates for assistant fork submit.", model: "ten-second-stream", }); await openAgentRoute(page, session); await expectComposerVisible(page); await awaitAssistantMessage(page); await session.client.waitForFinish(session.agentId, 45_000); await forkMostRecentAssistantTurnToNewWorkspace(page); await expectChatHistoryAttachment(page); await submitNewWorkspaceEmpty(page); const userMessage = page.getByTestId("user-message").filter({ hasText: "Chat history" }).last(); await expect(userMessage).toBeVisible({ timeout: 30_000 }); await expect(userMessage).not.toContainText("Source agent:"); }); });