session-timeline-tool-projection.spec.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  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("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
  10. const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
  11. const parts = ordinary.map((tool, index) =>
  12. toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
  13. )
  14. parts.push(
  15. toolPart("prt_question_dismissed", "question", "error", questionInput(), {
  16. error: "The user dismissed this question",
  17. }),
  18. toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }),
  19. toolPart("prt_todo_error", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }),
  20. )
  21. await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
  22. await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(ordinary.length + 1)
  23. await expect(page.getByText(/dismissed/i)).toBeVisible()
  24. await expect(page.locator('[data-timeline-part-id="prt_todo_error"]')).toHaveCount(0)
  25. for (let index = 0; index < ordinary.length; index++) {
  26. await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible()
  27. }
  28. })
  29. test("transitions shell and question through running error outcomes", async ({ page }) => {
  30. const shellID = "prt_transition_error_shell"
  31. const questionID = "prt_transition_error_question"
  32. const timeline = await setupTimeline(page, {
  33. messages: [
  34. userMessage(),
  35. assistantMessage(
  36. [
  37. toolPart(shellID, "bash", "pending", { command: "exit 1" }),
  38. toolPart(questionID, "question", "pending", questionInput()),
  39. ],
  40. { completed: false },
  41. ),
  42. ],
  43. })
  44. await timeline.waitForPart(shellID)
  45. await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
  46. await timeline.send(partUpdated(toolPart(shellID, "bash", "running", { command: "exit 1" })), 120)
  47. await timeline.send(partUpdated(toolPart(questionID, "question", "running", questionInput())), 180)
  48. await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
  49. await timeline.send(
  50. partUpdated(toolPart(shellID, "bash", "error", { command: "exit 1" }, { error: "Command exited 1" })),
  51. 180,
  52. )
  53. await timeline.send(
  54. partUpdated(
  55. toolPart(questionID, "question", "error", questionInput(), { error: "The user dismissed this question" }),
  56. ),
  57. 250,
  58. )
  59. await expect(page.locator(`[data-timeline-part-id="${shellID}"] [data-kind="tool-error-card"]`)).toBeVisible()
  60. await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText(/dismissed/i)
  61. })
  62. test("labels all web search provider variants", async ({ page }) => {
  63. const parts = [
  64. toolPart(
  65. "prt_search_parallel",
  66. "websearch",
  67. "completed",
  68. { query: "parallel" },
  69. { metadata: { provider: "parallel" } },
  70. ),
  71. toolPart("prt_search_exa", "websearch", "completed", { query: "exa" }, { metadata: { provider: "exa" } }),
  72. toolPart("prt_search_generic", "websearch", "completed", { query: "generic" }),
  73. ]
  74. await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
  75. await expect(page.getByRole("button", { name: /Parallel Web Search/ })).toBeVisible()
  76. await expect(page.getByRole("button", { name: /Exa Web Search/ })).toBeVisible()
  77. await expect(page.getByRole("button", { name: /^Web Search/ })).toBeVisible()
  78. })
  79. function questionInput() {
  80. return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] }
  81. }
  82. function errorInput(tool: string) {
  83. if (tool === "bash") return { command: "exit 1" }
  84. if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
  85. if (tool === "apply_patch") return { files: ["src/error.ts"] }
  86. if (tool === "webfetch") return { url: "https://example.com" }
  87. if (tool === "websearch") return { query: "failure" }
  88. if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
  89. if (tool === "skill") return { name: "failure" }
  90. return { target: "failure" }
  91. }