Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 18 additions & 55 deletions packages/opencode/test/agent/plugin-agent-regression.test.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,28 @@
import { expect } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Effect, Layer } from "effect"
import path from "path"
import { pathToFileURL } from "url"
import { Agent } from "../../src/agent/agent"
import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceLayer } from "../../src/project/instance-layer"
import { InstanceStore } from "../../src/project/instance-store"
import { tmpdirScoped } from "../fixture/fixture"
import { Plugin } from "../../src/plugin"
import { testEffect } from "../lib/effect"
import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants"

const pluginAgent = {
name: "plugin_added",
description: "Added by a plugin via the config hook",
mode: "subagent",
} as const
// `it.instance` skips InstanceBootstrap so FileWatcher / LSP / MCP don't spin
// up — those services hang during scope teardown on Windows and aren't needed
// to verify plugin → config hook → Agent.list.
const pluginUrl = pathToFileURL(path.join(import.meta.dir, "..", "fixture", "agent-plugin.ts")).href

const it = testEffect(Layer.mergeAll(Agent.defaultLayer, InstanceLayer.layer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(Layer.mergeAll(Agent.defaultLayer, Plugin.defaultLayer))

it.live("plugin-registered agents appear in Agent.list", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const pluginFile = path.join(dir, "plugin.ts")

yield* Effect.promise(async () => {
await Promise.all([
Bun.write(
pluginFile,
[
"export default async () => ({",
" config: async (cfg) => {",
" cfg.agent = cfg.agent ?? {}",
` cfg.agent[${JSON.stringify(pluginAgent.name)}] = {`,
` description: ${JSON.stringify(pluginAgent.description)},`,
` mode: ${JSON.stringify(pluginAgent.mode)},`,
" }",
" },",
"})",
"",
].join("\n"),
),
Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
plugin: [pathToFileURL(pluginFile).href],
}),
),
])
})

const agents = yield* InstanceStore.Service.use((store) =>
Effect.gen(function* () {
const ctx = yield* store.load({ directory: dir })
yield* Effect.addFinalizer(() => store.dispose(ctx).pipe(Effect.ignore))
return yield* Agent.Service.use((svc) => svc.list()).pipe(Effect.provideService(InstanceRef, ctx))
}),
)
const added = agents.find((agent) => agent.name === pluginAgent.name)

expect(added?.description).toBe(pluginAgent.description)
expect(added?.mode).toBe(pluginAgent.mode)
}),
it.instance(
"plugin-registered agents appear in Agent.list",
() =>
Effect.gen(function* () {
yield* Plugin.Service.use((p) => p.init())
const agents = yield* Agent.Service.use((svc) => svc.list())
const added = agents.find((agent) => agent.name === PLUGIN_AGENT.name)
expect(added?.description).toBe(PLUGIN_AGENT.description)
expect(added?.mode).toBe(PLUGIN_AGENT.mode)
}),
{ config: { plugin: [pluginUrl] } },
)
6 changes: 6 additions & 0 deletions packages/opencode/test/fixture/agent-plugin.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Separate file because every export in `agent-plugin.ts` must be a function.
export const PLUGIN_AGENT = {
name: "plugin_added",
description: "Added by a plugin via the config hook",
mode: "subagent",
} as const
12 changes: 12 additions & 0 deletions packages/opencode/test/fixture/agent-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Every export in this file must be a plugin function — `getLegacyPlugins`
// (src/plugin/index.ts) throws on anything else. Test constants live in
// `agent-plugin.constants.ts`.
export default async () => ({
config: async (cfg: { agent?: Record<string, unknown> }) => {
cfg.agent = cfg.agent ?? {}
cfg.agent["plugin_added"] = {
description: "Added by a plugin via the config hook",
mode: "subagent",
}
},
})
Loading