session-undo-redo.spec.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 messages = async () =>
  13. await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
  14. const seeded = await messages()
  15. const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
  16. const prompt = input.page.locator(promptSelector)
  17. await expect(prompt).toBeVisible()
  18. await input.sdk.session.promptAsync({
  19. sessionID: input.sessionID,
  20. noReply: true,
  21. parts: [{ type: "text", text: input.token }],
  22. })
  23. let userMessageID: string | undefined
  24. await expect
  25. .poll(
  26. async () => {
  27. const users = (await messages()).filter(
  28. (m) =>
  29. !userIDs.has(m.info.id) &&
  30. m.info.role === "user" &&
  31. m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
  32. )
  33. if (users.length === 0) return false
  34. const user = users[users.length - 1]
  35. if (!user) return false
  36. userMessageID = user.info.id
  37. return true
  38. },
  39. { timeout: 90_000, intervals: [250, 500, 1_000] },
  40. )
  41. .toBe(true)
  42. if (!userMessageID) throw new Error("Expected a user message id")
  43. await expect(input.page.locator(`[data-message-id="${userMessageID}"]`)).toHaveCount(1, { timeout: 30_000 })
  44. return { prompt, userMessageID }
  45. }
  46. test("slash undo sets revert and restores prior prompt", async ({ page, withBackendProject }) => {
  47. test.setTimeout(120_000)
  48. const token = `undo_${Date.now()}`
  49. await withBackendProject(async (project) => {
  50. const sdk = project.sdk
  51. await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
  52. project.trackSession(session.id)
  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, withBackendProject }) => {
  71. test.setTimeout(120_000)
  72. const token = `redo_${Date.now()}`
  73. await withBackendProject(async (project) => {
  74. const sdk = project.sdk
  75. await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
  76. project.trackSession(session.id)
  77. await project.gotoSession(session.id)
  78. const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
  79. await seeded.prompt.click()
  80. await page.keyboard.type("/undo")
  81. const undo = page.locator('[data-slash-id="session.undo"]').first()
  82. await expect(undo).toBeVisible()
  83. await page.keyboard.press("Enter")
  84. await expect
  85. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  86. timeout: 30_000,
  87. })
  88. .toBe(seeded.userMessageID)
  89. await seeded.prompt.click()
  90. await page.keyboard.press(`${modKey}+A`)
  91. await page.keyboard.press("Backspace")
  92. await page.keyboard.type("/redo")
  93. const redo = page.locator('[data-slash-id="session.redo"]').first()
  94. await expect(redo).toBeVisible()
  95. await page.keyboard.press("Enter")
  96. await expect
  97. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  98. timeout: 30_000,
  99. })
  100. .toBeUndefined()
  101. await expect(seeded.prompt).not.toContainText(token)
  102. await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(1)
  103. })
  104. })
  105. })
  106. test("slash undo/redo traverses multi-step revert stack", async ({ page, withBackendProject }) => {
  107. test.setTimeout(120_000)
  108. const firstToken = `undo_redo_first_${Date.now()}`
  109. const secondToken = `undo_redo_second_${Date.now()}`
  110. await withBackendProject(async (project) => {
  111. const sdk = project.sdk
  112. await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
  113. project.trackSession(session.id)
  114. await project.gotoSession(session.id)
  115. const first = await seedConversation({
  116. page,
  117. sdk,
  118. sessionID: session.id,
  119. token: firstToken,
  120. })
  121. const second = await seedConversation({
  122. page,
  123. sdk,
  124. sessionID: session.id,
  125. token: secondToken,
  126. })
  127. expect(first.userMessageID).not.toBe(second.userMessageID)
  128. const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
  129. const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
  130. await expect(firstMessage).toHaveCount(1)
  131. await expect(secondMessage).toHaveCount(1)
  132. await second.prompt.click()
  133. await page.keyboard.press(`${modKey}+A`)
  134. await page.keyboard.press("Backspace")
  135. await page.keyboard.type("/undo")
  136. const undo = page.locator('[data-slash-id="session.undo"]').first()
  137. await expect(undo).toBeVisible()
  138. await page.keyboard.press("Enter")
  139. await expect
  140. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  141. timeout: 30_000,
  142. })
  143. .toBe(second.userMessageID)
  144. await expect(firstMessage).toHaveCount(1)
  145. await expect(secondMessage).toHaveCount(0)
  146. await second.prompt.click()
  147. await page.keyboard.press(`${modKey}+A`)
  148. await page.keyboard.press("Backspace")
  149. await page.keyboard.type("/undo")
  150. await expect(undo).toBeVisible()
  151. await page.keyboard.press("Enter")
  152. await expect
  153. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  154. timeout: 30_000,
  155. })
  156. .toBe(first.userMessageID)
  157. await expect(firstMessage).toHaveCount(0)
  158. await expect(secondMessage).toHaveCount(0)
  159. await second.prompt.click()
  160. await page.keyboard.press(`${modKey}+A`)
  161. await page.keyboard.press("Backspace")
  162. await page.keyboard.type("/redo")
  163. const redo = page.locator('[data-slash-id="session.redo"]').first()
  164. await expect(redo).toBeVisible()
  165. await page.keyboard.press("Enter")
  166. await expect
  167. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  168. timeout: 30_000,
  169. })
  170. .toBe(second.userMessageID)
  171. await expect(firstMessage).toHaveCount(1)
  172. await expect(secondMessage).toHaveCount(0)
  173. await second.prompt.click()
  174. await page.keyboard.press(`${modKey}+A`)
  175. await page.keyboard.press("Backspace")
  176. await page.keyboard.type("/redo")
  177. await expect(redo).toBeVisible()
  178. await page.keyboard.press("Enter")
  179. await expect
  180. .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
  181. timeout: 30_000,
  182. })
  183. .toBeUndefined()
  184. await expect(firstMessage).toHaveCount(1)
  185. await expect(secondMessage).toHaveCount(1)
  186. })
  187. })
  188. })