plugin.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  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. draft: (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. yield* values.transform((editor) => {
  34. editor.add("scoped")
  35. })
  36. }),
  37. })
  38. expect(values.get().values).toEqual(["scoped"])
  39. yield* Scope.close(layerScope, Exit.void)
  40. expect(values.get().values).toEqual([])
  41. }),
  42. )
  43. it.effect("batches plugin state rebuilds when the registry layer finalizes", () =>
  44. Effect.gen(function* () {
  45. let finalized = 0
  46. const values = State.create({
  47. initial: () => ({ values: [] as string[] }),
  48. draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
  49. finalize: () => Effect.sync(() => finalized++),
  50. })
  51. const layerScope = yield* Scope.fork(yield* Scope.Scope)
  52. const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
  53. yield* State.batch(
  54. Effect.forEach(
  55. ["first", "second"],
  56. (id) =>
  57. plugin.add({
  58. id: PluginV2.ID.make(id),
  59. effect: values
  60. .transform((editor) => {
  61. editor.add(id)
  62. })
  63. .pipe(Effect.asVoid),
  64. }),
  65. { discard: true },
  66. ),
  67. )
  68. finalized = 0
  69. yield* Scope.close(layerScope, Exit.void)
  70. expect(values.get().values).toEqual([])
  71. expect(finalized).toBe(1)
  72. }),
  73. )
  74. it.effect("serializes same-ID additions and leaves one removable attachment", () =>
  75. Effect.gen(function* () {
  76. const values = state()
  77. const layerScope = yield* Scope.fork(yield* Scope.Scope)
  78. const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
  79. const id = PluginV2.ID.make("shared")
  80. const firstStarted = yield* Deferred.make<void>()
  81. const releaseFirst = yield* Deferred.make<void>()
  82. const first = yield* plugin
  83. .add({
  84. id,
  85. effect: Effect.gen(function* () {
  86. yield* values.transform((editor) => {
  87. editor.add("first")
  88. })
  89. yield* Deferred.succeed(firstStarted, undefined)
  90. yield* Deferred.await(releaseFirst)
  91. }),
  92. })
  93. .pipe(Effect.forkChild)
  94. yield* Deferred.await(firstStarted)
  95. const second = yield* plugin
  96. .add({
  97. id,
  98. effect: Effect.gen(function* () {
  99. yield* values.transform((editor) => {
  100. editor.add("second")
  101. })
  102. }),
  103. })
  104. .pipe(Effect.forkChild({ startImmediately: true }))
  105. expect(values.get().values).toEqual(["first"])
  106. yield* Deferred.succeed(releaseFirst, undefined)
  107. yield* Fiber.join(first)
  108. yield* Fiber.join(second)
  109. expect(values.get().values).toEqual(["second"])
  110. yield* plugin.remove(id)
  111. expect(values.get().values).toEqual([])
  112. }),
  113. )
  114. })