reload.test.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import path from "path"
  2. import { describe, expect } from "bun:test"
  3. import { Config as ConfigSchema } from "@opencode-ai/schema/config"
  4. import { AgentV2 } from "@opencode-ai/core/agent"
  5. import { Catalog } from "@opencode-ai/core/catalog"
  6. import { CommandV2 } from "@opencode-ai/core/command"
  7. import { Config } from "@opencode-ai/core/config"
  8. import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
  9. import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
  10. import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
  11. import { ConfigReferencePlugin } from "@opencode-ai/core/config/plugin/reference"
  12. import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill"
  13. import { EventV2 } from "@opencode-ai/core/event"
  14. import { Global } from "@opencode-ai/util/global"
  15. import { PluginV2 } from "@opencode-ai/core/plugin"
  16. import { PluginHost } from "@opencode-ai/core/plugin/host"
  17. import { ProviderV2 } from "@opencode-ai/core/provider"
  18. import { Reference } from "@opencode-ai/core/reference"
  19. import { SkillV2 } from "@opencode-ai/core/skill"
  20. import { Effect, Schema } from "effect"
  21. import { testEffect } from "../lib/effect"
  22. import { PluginTestLayer } from "../plugin/fixture"
  23. const it = testEffect(PluginTestLayer)
  24. const decode = Schema.decodeUnknownSync(Config.Info)
  25. const document = path.join(import.meta.dir, "opencode.json")
  26. describe("config plugin reloads", () => {
  27. it.live("reloads config-backed domains without reloading external plugins", () =>
  28. Effect.gen(function* () {
  29. const agents = yield* AgentV2.Service
  30. const catalog = yield* Catalog.Service
  31. const commands = yield* CommandV2.Service
  32. const events = yield* EventV2.Service
  33. const plugins = yield* PluginV2.Service
  34. const references = yield* Reference.Service
  35. const skills = yield* SkillV2.Service
  36. const host = yield* PluginHost.make(plugins)
  37. let entries: Config.Entry[] = [config("first")]
  38. const service = Config.Service.of({ entries: () => Effect.sync(() => entries) })
  39. const setup = <R>(effect: Effect.Effect<void, never, R>) =>
  40. effect.pipe(Effect.provideService(Config.Service, service))
  41. yield* setup(ConfigAgentPlugin.Plugin.effect(host))
  42. yield* setup(ConfigCommandPlugin.Plugin.effect(host))
  43. yield* setup(ConfigSkillPlugin.Plugin.effect(host))
  44. yield* setup(ConfigReferencePlugin.Plugin.effect(host))
  45. yield* setup(ConfigProviderPlugin.Plugin.effect(host))
  46. expect((yield* agents.get(AgentV2.ID.make("first")))?.description).toBe("First agent")
  47. expect((yield* commands.get("first"))?.description).toBe("First command")
  48. expect(
  49. (yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/first"),
  50. ).toBe(true)
  51. expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"])
  52. expect(yield* catalog.provider.get(ProviderV2.ID.make("first"))).toBeDefined()
  53. entries = [config("second")]
  54. yield* events.publish(ConfigSchema.Event.Updated, {})
  55. yield* waitUntil(
  56. Effect.gen(function* () {
  57. return (
  58. (yield* agents.get(AgentV2.ID.make("first"))) === undefined &&
  59. (yield* agents.get(AgentV2.ID.make("second")))?.description === "Second agent" &&
  60. (yield* commands.get("first")) === undefined &&
  61. (yield* commands.get("second"))?.description === "Second command" &&
  62. (yield* references.list()).some((reference) => reference.name === "second") &&
  63. (yield* catalog.provider.get(ProviderV2.ID.make("first"))) === undefined &&
  64. (yield* catalog.provider.get(ProviderV2.ID.make("second"))) !== undefined
  65. )
  66. }),
  67. )
  68. expect(
  69. (yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/first"),
  70. ).toBe(false)
  71. expect(
  72. (yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/second"),
  73. ).toBe(true)
  74. }).pipe(Effect.provideService(Global.Service, Global.Service.of(Global.make()))),
  75. )
  76. })
  77. function config(name: string) {
  78. return new Config.Document({
  79. type: "document",
  80. path: document,
  81. info: decode({
  82. agents: { [name]: { description: `${title(name)} agent`, mode: "subagent" } },
  83. commands: { [name]: { template: `${title(name)} command`, description: `${title(name)} command` } },
  84. skills: [`/skills/${name}`],
  85. references: { [name]: `/references/${name}` },
  86. providers: { [name]: { models: { chat: { name: `${title(name)} model` } } } },
  87. }),
  88. })
  89. }
  90. function title(value: string) {
  91. return value.charAt(0).toUpperCase() + value.slice(1)
  92. }
  93. const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect<boolean>) {
  94. for (let attempt = 0; attempt < 100; attempt++) {
  95. if (yield* condition) return
  96. yield* Effect.sleep("10 millis")
  97. }
  98. return yield* Effect.die("Timed out waiting for config plugin reloads")
  99. })