| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- 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, "opencode.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, "opencode.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, "opencode.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}`)
- })
|