context-matrix.spec.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import { expect, test } from "@playwright/test"
  2. import {
  3. defineVisualRegions,
  4. reportVisualStability,
  5. startVisualProbe,
  6. stopVisualProbe,
  7. visualPlan,
  8. } from "../../utils/visual-stability"
  9. import {
  10. assistantID,
  11. assistantMessage,
  12. event,
  13. partUpdated,
  14. setupTimeline,
  15. textPart,
  16. toolPart,
  17. userMessage,
  18. waitForVisualSettle,
  19. } from "./fixture"
  20. const inputs = {
  21. read: { filePath: "src/a.ts", offset: 0, limit: 120 },
  22. glob: { path: ".", pattern: "**/*.ts" },
  23. grep: { path: ".", pattern: "stable", include: "*.ts" },
  24. list: { path: "src" },
  25. }
  26. test("appends context operations while the group is expanded", async ({ page }, testInfo) => {
  27. const firstID = "prt_append_01_read"
  28. const followingID = "prt_append_99_following"
  29. const timeline = await setupTimeline(page, {
  30. messages: [
  31. userMessage(),
  32. assistantMessage([toolPart(firstID, "read", "running", inputs.read), textPart(followingID, "Following append")], {
  33. completed: false,
  34. }),
  35. ],
  36. cpuRate: 4,
  37. })
  38. const initialGroup = `[data-timeline-part-ids="${firstID}"]`
  39. await page.locator(`${initialGroup} [data-slot="collapsible-trigger"]`).click()
  40. await waitForVisualSettle(page, [initialGroup, `[data-timeline-part-id="${followingID}"]`])
  41. const regions = defineVisualRegions({
  42. context: {
  43. selector: '[data-timeline-part-ids^="prt_append_01_read"]',
  44. closest: '[data-timeline-row="AssistantPart"]',
  45. },
  46. following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
  47. })
  48. await startVisualProbe(page, regions)
  49. await timeline.send(partUpdated(toolPart("prt_append_02_glob", "glob", "running", inputs.glob)), 180)
  50. await timeline.send(partUpdated(toolPart("prt_append_03_grep", "grep", "completed", inputs.grep)), 240)
  51. await timeline.send(partUpdated(toolPart("prt_append_04_list", "list", "completed", inputs.list)), 500)
  52. const trace = await stopVisualProbe<keyof typeof regions>(page)
  53. await reportVisualStability(
  54. testInfo,
  55. "context-append",
  56. trace,
  57. visualPlan(
  58. regions,
  59. [
  60. { type: "required", regions: ["context", "following"] },
  61. { type: "unique", regions: ["context", "following"] },
  62. { type: "stable", regions: ["context", "following"] },
  63. { type: "opacity", regions: "all" },
  64. { type: "continuity", regions: "all" },
  65. { type: "motion", regions: ["following"], maxPositionReversals: 0 },
  66. { type: "label-stability", regions: "all" },
  67. { type: "preserve-bottom-anchor" },
  68. { type: "flow", regions: ["context", "following"] },
  69. ],
  70. { perMarker: true },
  71. ),
  72. )
  73. await expect(
  74. page.locator(
  75. '[data-timeline-part-ids="prt_append_01_read,prt_append_02_glob,prt_append_03_grep,prt_append_04_list"]',
  76. ),
  77. ).toBeVisible()
  78. await expect(
  79. page.locator('[data-timeline-part-ids^="prt_append_01_read"] [data-slot="collapsible-trigger"]'),
  80. ).toHaveAttribute("aria-expanded", "true")
  81. })
  82. test("splits and merges context groups when a middle text part changes", async ({ page }, testInfo) => {
  83. const textID = "prt_split_02_text"
  84. const followingID = "prt_split_99_following"
  85. const timeline = await setupTimeline(page, {
  86. messages: [
  87. userMessage(),
  88. assistantMessage([
  89. toolPart("prt_split_01_read", "read", "completed", inputs.read),
  90. textPart(textID, "Boundary"),
  91. toolPart("prt_split_03_glob", "glob", "completed", inputs.glob),
  92. textPart(followingID, "Following split groups"),
  93. ]),
  94. ],
  95. cpuRate: 4,
  96. })
  97. const regions = defineVisualRegions({
  98. following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
  99. })
  100. await startVisualProbe(page, regions)
  101. await timeline.send(
  102. event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: textID }),
  103. 500,
  104. )
  105. await expect(page.locator('[data-timeline-part-ids="prt_split_01_read,prt_split_03_glob"]')).toBeVisible()
  106. await timeline.send(partUpdated(textPart(textID, "Boundary restored")), 500)
  107. await expect(page.locator('[data-timeline-part-ids="prt_split_01_read"]')).toBeVisible()
  108. await expect(page.locator('[data-timeline-part-ids="prt_split_03_glob"]')).toBeVisible()
  109. const trace = await stopVisualProbe<keyof typeof regions>(page)
  110. await reportVisualStability(
  111. testInfo,
  112. "context-split-merge",
  113. trace,
  114. visualPlan(
  115. regions,
  116. [
  117. { type: "required", regions: ["following"] },
  118. { type: "unique", regions: ["following"] },
  119. { type: "stable", regions: ["following"] },
  120. { type: "opacity", regions: "all" },
  121. { type: "continuity", regions: "all" },
  122. { type: "motion", regions: "all", maxPositionReversals: 1 },
  123. { type: "label-stability", regions: "all" },
  124. ],
  125. { perMarker: true },
  126. ),
  127. )
  128. })
  129. test("removing the first context member replaces the group once without overlapping following content", async ({
  130. page,
  131. }, testInfo) => {
  132. const ids = ["prt_key_01_read", "prt_key_02_glob", "prt_key_03_grep"]
  133. const followingID = "prt_key_99_following"
  134. const timeline = await setupTimeline(page, {
  135. messages: [
  136. userMessage(),
  137. assistantMessage([
  138. toolPart(ids[0]!, "read", "completed", inputs.read),
  139. toolPart(ids[1]!, "glob", "completed", inputs.glob),
  140. toolPart(ids[2]!, "grep", "completed", inputs.grep),
  141. textPart(followingID, "Following replaced group"),
  142. ]),
  143. ],
  144. cpuRate: 4,
  145. })
  146. const original = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
  147. const originalRowKey = await original.evaluate((element) =>
  148. element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key"),
  149. )
  150. await original.locator('[data-slot="collapsible-trigger"]').click()
  151. await expect(original.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
  152. const regions = defineVisualRegions({
  153. context: {
  154. selector: '[data-timeline-part-ids*="prt_key_02_glob"]',
  155. closest: '[data-timeline-row="AssistantPart"]',
  156. },
  157. following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
  158. })
  159. await startVisualProbe(page, regions)
  160. await timeline.send(
  161. event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: ids[0] }),
  162. 500,
  163. )
  164. const trace = await stopVisualProbe<keyof typeof regions>(page)
  165. await reportVisualStability(
  166. testInfo,
  167. "context-first-remove",
  168. trace,
  169. visualPlan(regions, [
  170. { type: "required", regions: ["context", "following"] },
  171. { type: "unique", regions: ["context", "following"] },
  172. { type: "opacity", regions: "all" },
  173. { type: "continuity", regions: "all" },
  174. { type: "motion", regions: "all", maxPositionReversals: 0 },
  175. { type: "label-stability", regions: "all" },
  176. { type: "flow", regions: ["context", "following"] },
  177. ]),
  178. )
  179. await expect(page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`)).toBeVisible()
  180. expect(
  181. await page
  182. .locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`)
  183. .evaluate((element) => element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key")),
  184. ).toBe(originalRowKey)
  185. await expect(
  186. page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"] [data-slot="collapsible-trigger"]`),
  187. ).toHaveAttribute("aria-expanded", "true")
  188. })