plugin.test.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import { describe, expect } from "bun:test"
  2. import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
  3. import { EventV2 } from "@opencode-ai/core/event"
  4. import { PluginV2 } from "@opencode-ai/core/plugin"
  5. import { State } from "@opencode-ai/core/state"
  6. import { it } from "./lib/effect"
  7. const events = Layer.mock(EventV2.Service)({
  8. publish: (definition, data) =>
  9. Effect.succeed({
  10. id: EventV2.ID.make("evt_plugin_test"),
  11. type: definition.type,
  12. data,
  13. }),
  14. })
  15. const plugins = PluginV2.layer.pipe(Layer.provide(events))
  16. function state() {
  17. return State.create({
  18. initial: () => ({ values: [] as string[] }),
  19. editor: (draft) => ({
  20. add: (value: string) => draft.values.push(value),
  21. }),
  22. })
  23. }
  24. describe("PluginV2", () => {
  25. it.effect("closes plugin-owned scopes when the registry layer finalizes", () =>
  26. Effect.gen(function* () {
  27. const values = state()
  28. const layerScope = yield* Scope.fork(yield* Scope.Scope)
  29. const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
  30. yield* plugin.add({
  31. id: PluginV2.ID.make("scoped"),
  32. effect: Effect.gen(function* () {
  33. const transform = yield* values.transform()
  34. yield* transform((editor) => editor.add("scoped"))
  35. }),
  36. })
  37. expect(values.get().values).toEqual(["scoped"])
  38. yield* Scope.close(layerScope, Exit.void)
  39. expect(values.get().values).toEqual([])
  40. }),
  41. )
  42. it.effect("serializes same-ID additions and leaves one removable contribution", () =>
  43. Effect.gen(function* () {
  44. const values = state()
  45. const layerScope = yield* Scope.fork(yield* Scope.Scope)
  46. const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
  47. const id = PluginV2.ID.make("shared")
  48. const firstStarted = yield* Deferred.make<void>()
  49. const releaseFirst = yield* Deferred.make<void>()
  50. const first = yield* plugin
  51. .add({
  52. id,
  53. effect: Effect.gen(function* () {
  54. const transform = yield* values.transform()
  55. yield* transform((editor) => editor.add("first"))
  56. yield* Deferred.succeed(firstStarted, undefined)
  57. yield* Deferred.await(releaseFirst)
  58. }),
  59. })
  60. .pipe(Effect.forkChild)
  61. yield* Deferred.await(firstStarted)
  62. const second = yield* plugin
  63. .add({
  64. id,
  65. effect: Effect.gen(function* () {
  66. const transform = yield* values.transform()
  67. yield* transform((editor) => editor.add("second"))
  68. }),
  69. })
  70. .pipe(Effect.forkChild({ startImmediately: true }))
  71. expect(values.get().values).toEqual(["first"])
  72. yield* Deferred.succeed(releaseFirst, undefined)
  73. yield* Fiber.join(first)
  74. yield* Fiber.join(second)
  75. expect(values.get().values).toEqual(["second"])
  76. yield* plugin.remove(id)
  77. expect(values.get().values).toEqual([])
  78. }),
  79. )
  80. })