event-feed.test.ts 5.5 KB

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