file-matrix.spec.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { test } from "@playwright/test"
  2. import {
  3. defineVisualRegions,
  4. reportVisualStability,
  5. startVisualProbe,
  6. stopVisualProbe,
  7. visualPlan,
  8. } from "../../utils/visual-stability"
  9. import {
  10. assistantMessage,
  11. partUpdated,
  12. setupTimeline,
  13. textPart,
  14. toolPart,
  15. userMessage,
  16. waitForVisualSettle,
  17. } from "./fixture"
  18. const profiles = [
  19. { name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
  20. {
  21. name: "multi patch",
  22. tool: "apply_patch",
  23. input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
  24. },
  25. ] as const
  26. for (const profile of profiles) {
  27. test(`stabilizes ${profile.name} pending to completed`, async ({ page }, testInfo) => {
  28. const partID = `prt_file_matrix_${profiles.indexOf(profile)}`
  29. const followingID = `prt_file_matrix_following_${profiles.indexOf(profile)}`
  30. const timeline = await setupTimeline(page, {
  31. messages: [
  32. userMessage(),
  33. assistantMessage(
  34. [
  35. toolPart(partID, profile.tool, "pending", profile.input),
  36. textPart(followingID, `Following ${profile.name}`),
  37. ],
  38. { completed: false },
  39. ),
  40. ],
  41. settings: { editToolPartsExpanded: true },
  42. cpuRate: 4,
  43. })
  44. await waitForVisualSettle(page, [`[data-timeline-part-id="${partID}"]`, `[data-timeline-part-id="${followingID}"]`])
  45. const regions = defineVisualRegions({
  46. tool: { selector: `[data-timeline-part-id="${partID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
  47. following: {
  48. selector: `[data-timeline-part-id="${followingID}"]`,
  49. closest: '[data-timeline-row="AssistantPart"]',
  50. },
  51. })
  52. await startVisualProbe(page, regions)
  53. await timeline.send(partUpdated(toolPart(partID, profile.tool, "running", profile.input)), 180)
  54. await timeline.send(partUpdated(completedPart(partID, profile)), 900)
  55. const trace = await stopVisualProbe<keyof typeof regions>(page)
  56. await reportVisualStability(
  57. testInfo,
  58. `file-${profile.name}`,
  59. trace,
  60. visualPlan(
  61. regions,
  62. [
  63. { type: "required", regions: ["tool", "following"] },
  64. { type: "unique", regions: ["tool", "following"] },
  65. { type: "stable", regions: ["tool", "following"] },
  66. { type: "opacity", regions: "all" },
  67. { type: "continuity", regions: "all" },
  68. { type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 1 },
  69. { type: "label-stability", regions: "all" },
  70. { type: "preserve-bottom-anchor" },
  71. { type: "flow", regions: ["tool", "following"] },
  72. ],
  73. { perMarker: true },
  74. ),
  75. )
  76. })
  77. }
  78. function completedPart(partID: string, profile: (typeof profiles)[number]) {
  79. if (profile.tool === "edit") {
  80. return toolPart(partID, profile.tool, "completed", profile.input, {
  81. metadata: {
  82. filediff: {
  83. file: "src/edit.ts",
  84. additions: 50,
  85. deletions: 50,
  86. before: source(50, false),
  87. after: source(50, true),
  88. },
  89. },
  90. })
  91. }
  92. const files = [
  93. patchFile("src/a.ts", "update"),
  94. patchFile("src/b.ts", "add"),
  95. patchFile("src/old.ts", "delete"),
  96. { ...patchFile("src/moved.ts", "move"), move: "src/new-place.ts" },
  97. ]
  98. return toolPart(partID, profile.tool, "completed", profile.input, { metadata: { files } })
  99. }
  100. function patchFile(filePath: string, type: "add" | "update" | "delete" | "move") {
  101. return {
  102. filePath,
  103. relativePath: filePath,
  104. type,
  105. additions: type === "delete" ? 0 : 20,
  106. deletions: type === "add" ? 0 : 20,
  107. before: type === "add" ? undefined : source(20, false),
  108. after: type === "delete" ? undefined : source(20, true),
  109. }
  110. }
  111. function source(count: number, changed: boolean) {
  112. return Array.from(
  113. { length: count },
  114. (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`,
  115. ).join("")
  116. }