event-feed.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { describe, expect, test } from "bun:test"
  2. import { AgentV2 } from "@opencode-ai/core/agent"
  3. import { EventV2 } from "@opencode-ai/core/event"
  4. import { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
  5. import { DateTime, Deferred, Effect, Exit, Fiber, Option, Schema, Stream } from "effect"
  6. import { it } from "../../core/test/lib/effect"
  7. import { EventFeed } from "../src/event-feed"
  8. const Internal = EventV2.ephemeral({ type: "test.internal", schema: { value: Schema.String } })
  9. const event = (id: string): EventV2.Payload<typeof AgentV2.Event.Updated> => ({
  10. id: EventV2.ID.make(`evt_${id}`),
  11. created: DateTime.makeUnsafe(Date.now()),
  12. type: AgentV2.Event.Updated.type,
  13. data: {},
  14. })
  15. const internal = (value: string): EventV2.Payload<typeof Internal> => ({
  16. id: EventV2.ID.create(),
  17. created: DateTime.makeUnsafe(Date.now()),
  18. type: Internal.type,
  19. data: { value },
  20. })
  21. function makeSource() {
  22. let subscriber: EventV2.Subscriber | undefined
  23. return {
  24. observe: (next: EventV2.Subscriber) =>
  25. Effect.sync(() => {
  26. subscriber = next
  27. return Effect.sync(() => {
  28. if (subscriber === next) subscriber = undefined
  29. })
  30. }),
  31. publish: (event: EventV2.Payload) => Effect.suspend(() => (subscriber ? subscriber(event) : Effect.void)),
  32. }
  33. }
  34. describe("EventFeed", () => {
  35. test("preserves the public SSE frame encoding", () => {
  36. const payload = event("wire")
  37. expect(EventFeed.frame(payload)).toBe(
  38. `data: ${JSON.stringify(Schema.encodeUnknownSync(OpenCodeEvent)(payload))}\n\n`,
  39. )
  40. })
  41. it.effect("encodes once and delivers the same frame to every subscriber", () =>
  42. Effect.gen(function* () {
  43. let encodes = 0
  44. const source = makeSource()
  45. const feed = yield* EventFeed.make(source.observe, {
  46. encode: (event) => {
  47. encodes += 1
  48. return event.type
  49. },
  50. })
  51. const first = yield* feed.subscribe
  52. const second = yield* feed.subscribe
  53. const left = yield* first.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  54. const right = yield* second.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  55. yield* source.publish(event("example"))
  56. expect([Array.from(yield* Fiber.join(left)), Array.from(yield* Fiber.join(right))]).toEqual([
  57. [AgentV2.Event.Updated.type],
  58. [AgentV2.Event.Updated.type],
  59. ])
  60. expect(encodes).toBe(1)
  61. }),
  62. )
  63. it.effect("fails only the subscriber that exceeds its lag capacity", () =>
  64. Effect.gen(function* () {
  65. const source = makeSource()
  66. const feed = yield* EventFeed.make(source.observe, {
  67. capacity: 1,
  68. encode: (event) => event.id,
  69. })
  70. const slow = yield* feed.subscribe
  71. const fast = yield* feed.subscribe
  72. const first = yield* Deferred.make<void>()
  73. const second = yield* Deferred.make<void>()
  74. const received = new Array<string>()
  75. const fastFiber = yield* fast.pipe(
  76. Stream.take(3),
  77. Stream.runForEach((frame) =>
  78. Effect.sync(() => received.push(frame)).pipe(
  79. Effect.andThen(
  80. frame === "evt_one"
  81. ? Deferred.succeed(first, undefined)
  82. : frame === "evt_two"
  83. ? Deferred.succeed(second, undefined)
  84. : Effect.void,
  85. ),
  86. ),
  87. ),
  88. Effect.forkScoped,
  89. )
  90. yield* source.publish(event("one"))
  91. yield* Deferred.await(first)
  92. yield* source.publish(event("two"))
  93. yield* Deferred.await(second)
  94. yield* source.publish(event("three"))
  95. yield* Fiber.join(fastFiber)
  96. const result = yield* slow.pipe(Stream.runCollect, Effect.exit)
  97. expect(received).toEqual(["evt_one", "evt_two", "evt_three"])
  98. expect(Exit.isFailure(result)).toBeTrue()
  99. if (Exit.isSuccess(result)) return
  100. expect(Option.getOrUndefined(Exit.findErrorOption(result))).toBeInstanceOf(EventFeed.SubscriberOverflowError)
  101. }),
  102. )
  103. it.effect("filters internal events before they consume subscriber capacity", () =>
  104. Effect.gen(function* () {
  105. const source = makeSource()
  106. const feed = yield* EventFeed.make(source.observe, { capacity: 1, encode: (event) => event.type })
  107. const stream = yield* feed.subscribe
  108. yield* source.publish(internal("one"))
  109. yield* source.publish(internal("two"))
  110. yield* source.publish(event("public"))
  111. expect(Array.from(yield* stream.pipe(Stream.take(1), Stream.runCollect))).toEqual([AgentV2.Event.Updated.type])
  112. }),
  113. )
  114. it.effect("disconnects current subscribers after an encoding failure and continues for later subscribers", () =>
  115. Effect.gen(function* () {
  116. const source = makeSource()
  117. const feed = yield* EventFeed.make(source.observe, {
  118. encode: (event) => {
  119. if (event.id === EventV2.ID.make("evt_bad")) throw new Error("invalid event")
  120. return event.id
  121. },
  122. })
  123. const current = yield* feed.subscribe
  124. const failed = yield* current.pipe(Stream.runCollect, Effect.exit, Effect.forkScoped)
  125. yield* source.publish(event("bad"))
  126. const exit = yield* Fiber.join(failed)
  127. const next = yield* feed.subscribe
  128. const received = yield* next.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  129. yield* source.publish(event("good"))
  130. expect(Exit.isFailure(exit)).toBeTrue()
  131. if (Exit.isSuccess(exit)) return
  132. expect(Option.getOrUndefined(Exit.findErrorOption(exit))).toBeInstanceOf(EventFeed.EncodingError)
  133. expect(Array.from(yield* Fiber.join(received))).toEqual(["evt_good"])
  134. }),
  135. )
  136. })