diff --git a/packages/app/e2e/agent-message-rewind.spec.ts b/packages/app/e2e/agent-message-rewind.spec.ts index 7b8cd19263..b0797786fb 100644 --- a/packages/app/e2e/agent-message-rewind.spec.ts +++ b/packages/app/e2e/agent-message-rewind.spec.ts @@ -1,6 +1,12 @@ import type { Locator } from "@playwright/test"; import { expect, test, type Page } from "./fixtures"; -import { expectComposerDraft, expectComposerVisible, submitMessage } from "./helpers/composer"; +import { expectAgentIdle, expectAgentReadyToInterrupt } from "./helpers/agent-stream"; +import { + cancelAgent, + expectComposerDraft, + expectComposerVisible, + submitMessage, +} from "./helpers/composer"; import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; async function completeSubmittedTurn( @@ -25,6 +31,16 @@ async function rewindConversation(page: Page, userMessage: Locator, prompt: stri await page.getByRole("button", { name: "Rewind conversation", exact: true }).click(); } +async function expectTurnCompletesNormally( + page: Page, + agent: Awaited>, +): Promise { + const finish = await agent.client.waitForFinish(agent.agentId, 30_000); + expect(finish.status).toBe("idle"); + await expect(page.getByText("(end of synthetic stream)", { exact: true })).toBeVisible(); + await expectAgentIdle(page); +} + test.describe("Agent message rewind", () => { test("rewinds a submitted prompt and restores it to the composer", async ({ page }) => { test.setTimeout(90_000); @@ -48,4 +64,70 @@ test.describe("Agent message rewind", () => { await agent.cleanup(); } }); + + test("keeps a pre-acknowledgement turn running after rewind is rejected", async ({ page }) => { + test.setTimeout(90_000); + const prompt = "Delay synthetic user message by 2000ms."; + const rewindError = "Cannot rewind before the provider acknowledges the submitted prompt"; + const agent = await seedMockAgentWorkspace({ + repoPrefix: "message-rewind-pre-echo-", + title: "Message rewind pre-echo", + model: "ten-second-stream", + }); + + try { + await openAgentRoute(page, agent); + await expectComposerVisible(page); + await submitMessage(page, prompt); + const userMessage = page.getByTestId("user-message").filter({ hasText: prompt }); + await expect(userMessage).toBeVisible(); + await expectAgentReadyToInterrupt(page); + + await rewindConversation(page, userMessage, prompt); + + await expect(page.getByText(rewindError, { exact: true })).toBeVisible(); + await expectAgentReadyToInterrupt(page); + await expectTurnCompletesNormally(page, agent); + } finally { + await agent.cleanup(); + } + }); + + test("does not offer rewind for daemon-handled prompts", async ({ page }) => { + const commandPrompt = "/mock handled-command"; + const providerPrompt = "Keep this provider-bound prompt rewindable."; + const agent = await seedMockAgentWorkspace({ + repoPrefix: "message-rewind-daemon-command-", + title: "Message rewind daemon command", + model: "ten-second-stream", + }); + + try { + await openAgentRoute(page, agent); + await expectComposerVisible(page); + await expectAgentIdle(page); + + await submitMessage(page, commandPrompt); + const commandMessage = page.getByTestId("user-message").filter({ hasText: commandPrompt }); + await expect(commandMessage).toBeVisible(); + await expect(page.getByText("Mock command handled", { exact: true })).toBeVisible(); + await commandMessage.getByText(commandPrompt, { exact: true }).hover(); + await expect( + commandMessage.getByRole("button", { name: "Rewind to this message", exact: true }), + ).toHaveCount(0); + + await submitMessage(page, providerPrompt); + const providerMessage = page.getByTestId("user-message").filter({ hasText: providerPrompt }); + await expect(providerMessage).toBeVisible(); + await expectAgentReadyToInterrupt(page); + await providerMessage.getByText(providerPrompt, { exact: true }).hover(); + await expect( + providerMessage.getByRole("button", { name: "Rewind to this message", exact: true }), + ).toBeVisible(); + await cancelAgent(page); + await expectAgentIdle(page); + } finally { + await agent.cleanup(); + } + }); });