2
0

session-undo-redo.spec.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import type { Page } from "@playwright/test"
  2. import { test, expect } from "../fixtures"
  3. import { withSession } from "../actions"
  4. import { createSdk, modKey } from "../utils"
  5. import { promptSelector } from "../selectors"
  6. async function seedConversation(input: {
  7. page: Page
  8. sdk: ReturnType<typeof createSdk>
  9. sessionID: string
  10. token: string
  11. }) {
  12. const prompt = input.page.locator(promptSelector)
  13. await expect(prompt).toBeVisible()
  14. await prompt.click()
  15. await input.page.keyboard.type(`Reply with exactly: ${input.token}`)
  16. await input.page.keyboard.press("Enter")
  17. let userMessageID: string | undefined
  18. await expect
  19. .poll(
  20. async () => {
  21. const messages = await input.sdk.session
  22. .messages({ sessionID: input.sessionID, limit: 50 })
  23. .then((r) => r.data ?? [])
  24. const users = messages.filter(
  25. (m) =>
  26. m.info.role === "user" &&
  27. m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
  28. )
  29. if (users.length === 0) return false
  30. const user = users[users.length - 1]
  31. if (!user) return false
  32. userMessageID = user.info.id
  33. const assistantText = messages
  34. .filter((m) => m.info.role === "assistant")
  35. .flatMap((m) => m.parts)
  36. .filter((p) => p.type === "text")
  37. .map((p) => p.text)
  38. .join("\n")
  39. return assistantText.includes(input.token)
  40. },
  41. { timeout: 90_000 },
  42. )
  43. .toBe(true)
  44. if (!userMessageID) throw new Error("Expected a user message id")
  45. return { prompt, userMessageID }
  46. }
  47. test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => {
  48. test.setTimeout(120_000)
  49. const token = `undo_${Date.now()}`
  50. await withProject(async (project) => {
  51. const sdk = createSdk(project.directory)
  52. await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
  53. await project.gotoSession(session.id)
  54. const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
  55. await seeded.prompt.click()
  56. await page.keyboard.type("/undo")
  57. const undo = page.locator('[data-slash-id="session.undo"]').first()
  58. await expect(undo).toBeVisible()
  59. await page.keyboard.press("Enter")
  60. await expect
  61. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  62. timeout: 30_000,
  63. })
  64. .toBe(seeded.userMessageID)
  65. await expect(seeded.prompt).toContainText(token)
  66. await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
  67. })
  68. })
  69. })
  70. test("slash redo clears revert and restores latest state", async ({ page, withProject }) => {
  71. test.setTimeout(120_000)
  72. const token = `redo_${Date.now()}`
  73. await withProject(async (project) => {
  74. const sdk = createSdk(project.directory)
  75. await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
  76. await project.gotoSession(session.id)
  77. const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
  78. await seeded.prompt.click()
  79. await page.keyboard.type("/undo")
  80. const undo = page.locator('[data-slash-id="session.undo"]').first()
  81. await expect(undo).toBeVisible()
  82. await page.keyboard.press("Enter")
  83. await expect
  84. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  85. timeout: 30_000,
  86. })
  87. .toBe(seeded.userMessageID)
  88. await seeded.prompt.click()
  89. await page.keyboard.press(`${modKey}+A`)
  90. await page.keyboard.press("Backspace")
  91. await page.keyboard.type("/redo")
  92. const redo = page.locator('[data-slash-id="session.redo"]').first()
  93. await expect(redo).toBeVisible()
  94. await page.keyboard.press("Enter")
  95. await expect
  96. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  97. timeout: 30_000,
  98. })
  99. .toBeUndefined()
  100. await expect(seeded.prompt).not.toContainText(token)
  101. await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`).first()).toBeVisible()
  102. })
  103. })
  104. })
  105. test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => {
  106. test.setTimeout(120_000)
  107. const firstToken = `undo_redo_first_${Date.now()}`
  108. const secondToken = `undo_redo_second_${Date.now()}`
  109. await withProject(async (project) => {
  110. const sdk = createSdk(project.directory)
  111. await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
  112. await project.gotoSession(session.id)
  113. const first = await seedConversation({
  114. page,
  115. sdk,
  116. sessionID: session.id,
  117. token: firstToken,
  118. })
  119. const second = await seedConversation({
  120. page,
  121. sdk,
  122. sessionID: session.id,
  123. token: secondToken,
  124. })
  125. expect(first.userMessageID).not.toBe(second.userMessageID)
  126. const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
  127. const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
  128. await expect(firstMessage.first()).toBeVisible()
  129. await expect(secondMessage.first()).toBeVisible()
  130. await second.prompt.click()
  131. await page.keyboard.press(`${modKey}+A`)
  132. await page.keyboard.press("Backspace")
  133. await page.keyboard.type("/undo")
  134. const undo = page.locator('[data-slash-id="session.undo"]').first()
  135. await expect(undo).toBeVisible()
  136. await page.keyboard.press("Enter")
  137. await expect
  138. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  139. timeout: 30_000,
  140. })
  141. .toBe(second.userMessageID)
  142. await expect(firstMessage.first()).toBeVisible()
  143. await expect(secondMessage).toHaveCount(0)
  144. await second.prompt.click()
  145. await page.keyboard.press(`${modKey}+A`)
  146. await page.keyboard.press("Backspace")
  147. await page.keyboard.type("/undo")
  148. await expect(undo).toBeVisible()
  149. await page.keyboard.press("Enter")
  150. await expect
  151. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  152. timeout: 30_000,
  153. })
  154. .toBe(first.userMessageID)
  155. await expect(firstMessage).toHaveCount(0)
  156. await expect(secondMessage).toHaveCount(0)
  157. await second.prompt.click()
  158. await page.keyboard.press(`${modKey}+A`)
  159. await page.keyboard.press("Backspace")
  160. await page.keyboard.type("/redo")
  161. const redo = page.locator('[data-slash-id="session.redo"]').first()
  162. await expect(redo).toBeVisible()
  163. await page.keyboard.press("Enter")
  164. await expect
  165. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  166. timeout: 30_000,
  167. })
  168. .toBe(second.userMessageID)
  169. await expect(firstMessage.first()).toBeVisible()
  170. await expect(secondMessage).toHaveCount(0)
  171. await second.prompt.click()
  172. await page.keyboard.press(`${modKey}+A`)
  173. await page.keyboard.press("Backspace")
  174. await page.keyboard.type("/redo")
  175. await expect(redo).toBeVisible()
  176. await page.keyboard.press("Enter")
  177. await expect
  178. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  179. timeout: 30_000,
  180. })
  181. .toBeUndefined()
  182. await expect(firstMessage.first()).toBeVisible()
  183. await expect(secondMessage.first()).toBeVisible()
  184. })
  185. })
  186. })