session-timeline-transport.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { expect, test } from "@playwright/test"
  2. import {
  3. assistantMessage,
  4. partUpdated,
  5. setupTimeline,
  6. status,
  7. textPart,
  8. userMessage,
  9. } from "../performance/timeline-stability/fixture"
  10. test("keeps one connection open while delivering multiple events", async ({ page }) => {
  11. const timeline = await setupTimeline(page)
  12. const first = await timeline.transport.send(partUpdated(textPart("prt_transport_first", "first event")))
  13. const second = await timeline.transport.send(partUpdated(textPart("prt_transport_second", "second event")))
  14. await timeline.waitForPart("prt_transport_first")
  15. await timeline.waitForPart("prt_transport_second")
  16. expect(first.connectionID).toBe(second.connectionID)
  17. expect(await timeline.transport.connections()).toHaveLength(1)
  18. expect(await timeline.transport.acknowledgements()).toHaveLength(2)
  19. })
  20. test("delivers a burst from one stream chunk", async ({ page }) => {
  21. const timeline = await setupTimeline(page)
  22. const acknowledgements = await timeline.transport.burst([
  23. partUpdated(textPart("prt_transport_burst_a", "burst a")),
  24. partUpdated(textPart("prt_transport_burst_b", "burst b")),
  25. ])
  26. await timeline.waitForPart("prt_transport_burst_a")
  27. await timeline.waitForPart("prt_transport_burst_b")
  28. expect(acknowledgements.map((item) => item.chunkCount)).toEqual([1, 1])
  29. expect(new Set(acknowledgements.map((item) => item.deliveryID)).size).toBe(2)
  30. })
  31. test("parses split JSON and a split multibyte code point", async ({ page }) => {
  32. const timeline = await setupTimeline(page)
  33. const payload = partUpdated(textPart("prt_transport_split", "split snowman \u2603\u2603\u2603"))
  34. const encoded = new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`)
  35. const snowman = new TextEncoder().encode("\u2603")[0]!
  36. const multibyte = encoded.indexOf(snowman)
  37. const acknowledgement = await timeline.transport.split(payload, [9, multibyte + 1, multibyte + 2])
  38. await timeline.waitForPart("prt_transport_split")
  39. await expect(page.locator('[data-timeline-part-id="prt_transport_split"]')).toContainText(
  40. "split snowman \u2603\u2603\u2603",
  41. )
  42. expect(acknowledgement.chunkCount).toBe(4)
  43. })
  44. test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
  45. const timeline = await setupTimeline(page, {
  46. messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
  47. })
  48. const before = await page.locator("[data-timeline-row]").allTextContents()
  49. await timeline.transport.heartbeat()
  50. await timeline.settle()
  51. expect(await page.locator("[data-timeline-row]").allTextContents()).toEqual(before)
  52. expect(await timeline.transport.connections()).toHaveLength(1)
  53. })
  54. test("reconnects after a clean close", async ({ page }) => {
  55. const timeline = await setupTimeline(page, { eventRetry: 10 })
  56. const first = await timeline.transport.waitForConnection()
  57. await timeline.transport.close()
  58. const second = await timeline.transport.waitForConnection({ after: first.id })
  59. await timeline.transport.send(partUpdated(textPart("prt_transport_close", "after close")))
  60. await timeline.waitForPart("prt_transport_close")
  61. expect(second.id).toBeGreaterThan(first.id)
  62. expect((await timeline.transport.connections())[0]?.endedBy).toBe("close")
  63. })
  64. test("reconnects after a stream error", async ({ page }) => {
  65. const timeline = await setupTimeline(page, { eventRetry: 10 })
  66. const first = await timeline.transport.waitForConnection()
  67. await timeline.transport.error("contract failure")
  68. const second = await timeline.transport.waitForConnection({ after: first.id })
  69. await timeline.transport.send(status("busy"))
  70. await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
  71. expect(second.id).toBeGreaterThan(first.id)
  72. expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
  73. })
  74. test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => {
  75. const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" })
  76. const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), {
  77. id: "timeline-event-7",
  78. })
  79. await timeline.waitForPart("prt_transport_id")
  80. await timeline.transport.error("retry with event id")
  81. const connection = await timeline.transport.waitForConnection({ after: first.connectionID })
  82. expect(first.eventID).toBe("timeline-event-7")
  83. expect(connection.headers["last-event-id"]).toBeUndefined()
  84. })
  85. test("passes through non-event fetches", async ({ page }) => {
  86. const timeline = await setupTimeline(page)
  87. const health = await page.evaluate(async () => {
  88. const response = await fetch("/global/health")
  89. return response.json()
  90. })
  91. expect(health).toEqual({ healthy: true })
  92. expect(await timeline.transport.connections()).toHaveLength(1)
  93. })