import path from "path" import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external" import { FSUtil } from "@opencode-ai/core/fs-util" import { Location } from "@opencode-ai/core/location" import { Npm } from "@opencode-ai/core/npm" import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AbsolutePath } from "@opencode-ai/core/schema" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" const it = testEffect(PluginTestLayer) const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigExternalPlugin", () => { it.live("resolves and loads a configured Promise plugin with options", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const agents = yield* AgentV2.Service const fs = yield* FSUtil.Service const location = yield* Location.Service const npm = yield* Npm.Service const host = yield* PluginHost.make(plugins) const document = path.join(import.meta.dir, "config.json") yield* ConfigExternalPlugin.Plugin.effect(host).pipe( Effect.provideService(PluginV2.Service, plugins), Effect.provideService(FSUtil.Service, fs), Effect.provideService(Location.Service, location), Effect.provideService(Npm.Service, npm), Effect.provideService( Config.Service, Config.Service.of({ entries: () => Effect.succeed([ new Config.Document({ type: "document", path: document, info: decode({ plugins: [ { package: "../plugin/fixtures/config-promise-plugin.ts", options: { description: "Loaded from config" }, }, ], }), }), ]), }), ), ) expect(yield* waitForAgent(agents, "configured")).toMatchObject({ description: "Loaded from config", mode: "subagent", }) }), ) it.live("loads a configured Effect plugin with options", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const agents = yield* AgentV2.Service const fs = yield* FSUtil.Service const location = yield* Location.Service const npm = yield* Npm.Service const host = yield* PluginHost.make(plugins) yield* ConfigExternalPlugin.Plugin.effect(host).pipe( Effect.provideService(PluginV2.Service, plugins), Effect.provideService(FSUtil.Service, fs), Effect.provideService(Location.Service, location), Effect.provideService(Npm.Service, npm), Effect.provideService( Config.Service, Config.Service.of({ entries: () => Effect.succeed([ new Config.Document({ type: "document", path: path.join(import.meta.dir, "config.json"), info: decode({ plugins: [ { package: "../plugin/fixtures/config-effect-plugin.ts", options: { description: "Effect plugin from config" }, }, ], }), }), ]), }), ), ) expect(yield* waitForAgent(agents, "effect-configured")).toMatchObject({ description: "Effect plugin from config", mode: "subagent", }) }), ) it.live("ignores invalid plugins and continues loading", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const agents = yield* AgentV2.Service const fs = yield* FSUtil.Service const location = yield* Location.Service const npm = yield* Npm.Service const host = yield* PluginHost.make(plugins) yield* ConfigExternalPlugin.Plugin.effect(host).pipe( Effect.provideService(PluginV2.Service, plugins), Effect.provideService(FSUtil.Service, fs), Effect.provideService(Location.Service, location), Effect.provideService(Npm.Service, npm), Effect.provideService( Config.Service, Config.Service.of({ entries: () => Effect.succeed([ new Config.Document({ type: "document", path: path.join(import.meta.dir, "config.json"), info: decode({ plugins: [ "../plugin/fixtures/missing-plugin.ts", "../plugin/fixtures/invalid-plugin.ts", { package: "../plugin/fixtures/config-promise-plugin.ts", options: { description: "Loaded after invalid plugins" }, }, ], }), }), ]), }), ), ) expect(yield* waitForAgent(agents, "configured")).toMatchObject({ description: "Loaded after invalid plugins", }) }), ) it.live("installs and resolves npm plugin packages", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const agents = yield* AgentV2.Service const fs = yield* FSUtil.Service const location = yield* Location.Service const host = yield* PluginHost.make(plugins) let installed: string | undefined const npm = Npm.Service.of({ add: (spec) => Effect.sync(() => { installed = spec return { directory: import.meta.dir, entrypoint: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"), } }), install: () => Effect.void, which: () => Effect.succeed(undefined), }) yield* ConfigExternalPlugin.Plugin.effect(host).pipe( Effect.provideService(PluginV2.Service, plugins), Effect.provideService(FSUtil.Service, fs), Effect.provideService(Location.Service, location), Effect.provideService(Npm.Service, npm), Effect.provideService( Config.Service, Config.Service.of({ entries: () => Effect.succeed([ new Config.Document({ type: "document", info: decode({ plugins: [ { package: "example-plugin@1.0.0", options: { description: "Installed from npm" }, }, ], }), }), ]), }), ), ) expect(yield* waitForAgent(agents, "configured")).toMatchObject({ description: "Installed from npm", }) expect(installed).toBe("example-plugin@1.0.0") }), ) it.live("loads plugin files from config directories", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const agents = yield* AgentV2.Service const fs = yield* FSUtil.Service const location = yield* Location.Service const npm = yield* Npm.Service const host = yield* PluginHost.make(plugins) yield* ConfigExternalPlugin.Plugin.effect(host).pipe( Effect.provideService(PluginV2.Service, plugins), Effect.provideService(FSUtil.Service, fs), Effect.provideService(Location.Service, location), Effect.provideService(Npm.Service, npm), Effect.provideService( Config.Service, Config.Service.of({ entries: () => Effect.succeed([ new Config.Directory({ type: "directory", path: AbsolutePath.make(path.join(import.meta.dir, "fixtures")), }), ]), }), ), ) expect(yield* waitForAgent(agents, "directory")).toMatchObject({ description: "Loaded from plugin directory", mode: "subagent", }) }), ) }) const waitForAgent = Effect.fnUntraced(function* (agents: AgentV2.Interface, id: string) { for (let attempt = 0; attempt < 100; attempt++) { const agent = yield* agents.get(AgentV2.ID.make(id)) if (agent) return agent yield* Effect.sleep("10 millis") } return yield* Effect.die(`Timed out waiting for agent ${id}`) })