session-timeline-lifecycle-state.spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { expect, test } from "@playwright/test"
  2. import {
  3. assistantMessage,
  4. completedAssistantInfo,
  5. messageUpdated,
  6. partUpdated,
  7. reasoningPart,
  8. setupTimeline,
  9. shell,
  10. status,
  11. textPart,
  12. userMessage,
  13. } from "../performance/timeline-stability/fixture"
  14. for (const expanded of [false, true]) {
  15. test(`preserves shell user intent from a ${expanded ? "expanded" : "collapsed"} default`, async ({ page }) => {
  16. const id = `prt_shell_default_${expanded}`
  17. const timeline = await setupTimeline(page, {
  18. messages: [userMessage(), assistantMessage([shell(id, "completed", lines(3))])],
  19. settings: { shellToolPartsExpanded: expanded },
  20. })
  21. const trigger = page.locator(`[data-timeline-part-id="${id}"] [data-slot="collapsible-trigger"]`)
  22. await expect(trigger).toHaveAttribute("aria-expanded", String(expanded))
  23. await trigger.click()
  24. await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded))
  25. await timeline.send(partUpdated(shell(id, "completed", lines(6))), 180)
  26. await timeline.send(partUpdated(textPart(`prt_sibling_${expanded}`, "Sibling content")), 180)
  27. await timeline.send(status("busy"), 100)
  28. await timeline.send(status("idle"), 250)
  29. await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded))
  30. })
  31. }
  32. test("shows and expands a running shell command without shimmering it", async ({ page }) => {
  33. const id = "prt_shell_running_command"
  34. const command = "sleep 10 && echo done"
  35. await setupTimeline(page, {
  36. messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })],
  37. settings: { shellToolPartsExpanded: false },
  38. })
  39. const tool = page.locator(`[data-timeline-part-id="${id}"]`)
  40. await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true")
  41. await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command)
  42. await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0)
  43. await tool.locator('[data-slot="collapsible-trigger"]').click()
  44. await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
  45. await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running")
  46. })
  47. test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => {
  48. const reasoningID = "prt_reasoning_hidden"
  49. const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false })
  50. const timeline = await setupTimeline(page, {
  51. messages: [userMessage(), assistant],
  52. settings: { showReasoningSummaries: false },
  53. cpuRate: 4,
  54. })
  55. await timeline.send(status("busy"), 150)
  56. await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
  57. await expect(page.getByText("Inspecting stability", { exact: true })).toBeVisible()
  58. await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
  59. await timeline.send(partUpdated(shell("prt_reasoning_shell", "running")), 160)
  60. await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
  61. await timeline.send(partUpdated(shell("prt_reasoning_shell", "completed", "done")), 180)
  62. await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100)
  63. await timeline.send(status("idle"), 300)
  64. await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
  65. await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
  66. })
  67. test("moves busy through retry and recovery to final idle content", async ({ page }) => {
  68. const assistant = assistantMessage([], { completed: false })
  69. const timeline = await setupTimeline(page, {
  70. messages: [
  71. userMessage(undefined, {
  72. summary: {
  73. diffs: [
  74. {
  75. file: "src/retry.ts",
  76. additions: 1,
  77. deletions: 1,
  78. patch: "@@ -1 +1 @@\n-export const retry = false\n+export const retry = true",
  79. },
  80. ],
  81. },
  82. }),
  83. assistant,
  84. ],
  85. })
  86. await timeline.send(status("busy"), 140)
  87. await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
  88. await expect(page.locator('[data-timeline-row="DiffSummary"]')).toHaveCount(0)
  89. await timeline.send(status("retry"), 180)
  90. await expect(page.locator('[data-timeline-row="Retry"]')).toBeVisible()
  91. await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
  92. await timeline.send(status("busy", 2), 180)
  93. await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
  94. await timeline.send(partUpdated(textPart("prt_recovered", "Recovered response")), 140)
  95. await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100)
  96. await timeline.send(status("idle"), 350)
  97. await expect(page.locator('[data-timeline-row="Retry"]')).toHaveCount(0)
  98. await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
  99. await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
  100. })
  101. function lines(count: number) {
  102. return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
  103. }