session-timeline-file-state.spec.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { expect, test } from "@playwright/test"
  2. import {
  3. assistantMessage,
  4. partUpdated,
  5. setupTimeline,
  6. toolPart,
  7. userMessage,
  8. } from "../performance/timeline-stability/fixture"
  9. test("updates edit diagnostics without resetting manual collapse state", async ({ page }) => {
  10. const editID = "prt_diagnostics_edit"
  11. const base = editPart(editID, [])
  12. const timeline = await setupTimeline(page, {
  13. messages: [userMessage(), assistantMessage([base])],
  14. settings: { editToolPartsExpanded: true },
  15. })
  16. const trigger = page.locator(`[data-timeline-part-id="${editID}"] [data-slot="collapsible-trigger"]`).first()
  17. await trigger.click()
  18. await expect(trigger).toHaveAttribute("aria-expanded", "false")
  19. await timeline.send(
  20. partUpdated(editPart(editID, [diagnostic("First failure", 2), diagnostic("Second failure", 4)])),
  21. 300,
  22. )
  23. await expect(trigger).toHaveAttribute("aria-expanded", "false")
  24. await timeline.send(partUpdated(editPart(editID, [])), 300)
  25. await expect(trigger).toHaveAttribute("aria-expanded", "false")
  26. })
  27. test("preserves nested patch file state through outer collapse and reopen", async ({ page }) => {
  28. const patchID = "prt_nested_patch"
  29. const files = [patchFile("src/a.ts", "update"), patchFile("src/b.ts", "add"), patchFile("src/old.ts", "delete")]
  30. await setupTimeline(page, {
  31. messages: [
  32. userMessage(),
  33. assistantMessage([
  34. toolPart(
  35. patchID,
  36. "apply_patch",
  37. "completed",
  38. { files: files.map((file) => file.filePath) },
  39. { metadata: { files } },
  40. ),
  41. ]),
  42. ],
  43. settings: { editToolPartsExpanded: true },
  44. })
  45. const wrapper = page.locator(`[data-timeline-part-id="${patchID}"]`)
  46. const outer = wrapper.locator('[data-slot="collapsible-trigger"]').first()
  47. const deleted = wrapper.locator('[data-scope="apply-patch"] [data-type="delete"]')
  48. await deleted.getByRole("button").click()
  49. await expect(deleted.getByRole("button")).toHaveAttribute("aria-expanded", "true")
  50. await outer.click()
  51. await expect(outer).toHaveAttribute("aria-expanded", "false")
  52. await outer.click()
  53. await expect(outer).toHaveAttribute("aria-expanded", "true")
  54. await expect(deleted.getByRole("button")).toHaveAttribute("aria-expanded", "true")
  55. })
  56. function patchFile(filePath: string, type: "add" | "update" | "delete") {
  57. return {
  58. filePath,
  59. relativePath: filePath,
  60. type,
  61. additions: type === "delete" ? 0 : 4,
  62. deletions: type === "add" ? 0 : 3,
  63. before: type === "add" ? undefined : source(false),
  64. after: type === "delete" ? undefined : source(true),
  65. }
  66. }
  67. function editPart(id: string, diagnostics: Record<string, unknown>[]) {
  68. return toolPart(
  69. id,
  70. "edit",
  71. "completed",
  72. { filePath: "src/edit.ts" },
  73. {
  74. metadata: {
  75. filediff: { file: "src/edit.ts", additions: 1, deletions: 1, before: source(false), after: source(true) },
  76. diagnostics,
  77. },
  78. },
  79. )
  80. }
  81. function diagnostic(message: string, line: number) {
  82. return { message, severity: 1, range: { start: { line, character: 0 }, end: { line, character: 2 } } }
  83. }
  84. function source(changed: boolean) {
  85. return Array.from({ length: 12 }, (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`).join(
  86. "",
  87. )
  88. }