Skip to content
Merged
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
111 changes: 111 additions & 0 deletions packages/opencode/test/plugin/trigger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { afterAll, afterEach, describe, expect, test } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../fixture/fixture"

const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"

const { Plugin } = await import("../../src/plugin/index")
const { Instance } = await import("../../src/project/instance")

afterEach(async () => {
await Instance.disposeAll()
})

afterAll(() => {
if (disableDefault === undefined) {
delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
return
}
process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault
})

async function project(source: string) {
return tmpdir({
init: async (dir) => {
const file = path.join(dir, "plugin.ts")
await Bun.write(file, source)
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify(
{
$schema: "https://opencode.ai/config.json",
plugin: [pathToFileURL(file).href],
},
null,
2,
),
)
},
})
}

describe("plugin.trigger", () => {
test("runs synchronous hooks without crashing", async () => {
await using tmp = await project(
[
"export default async () => ({",
' "experimental.chat.system.transform": (_input, output) => {',
' output.system.unshift("sync")',
" },",
"})",
"",
].join("\n"),
)

const out = await Instance.provide({
directory: tmp.path,
fn: async () => {
const out = { system: [] as string[] }
await Plugin.trigger(
"experimental.chat.system.transform",
{
model: {
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
} as any,
},
Comment on lines +63 to +68
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook type for "experimental.chat.system.transform" expects input.model to be a full SDK Model (per the Hooks definition in packages/plugin/src/index.ts), but the test passes { providerID, modelID } cast as any. This makes the regression less representative and could hide failures in plugins that legitimately read fields like model.id; consider passing a more realistic Model shape (or resolving one via the same provider/model helper used in runtime) so the test matches real inputs.

Copilot uses AI. Check for mistakes.
out,
)
return out
},
})

expect(out.system).toEqual(["sync"])
})

test("awaits asynchronous hooks", async () => {
await using tmp = await project(
[
"export default async () => ({",
' "experimental.chat.system.transform": async (_input, output) => {',
" await Bun.sleep(1)",
' output.system.unshift("async")',
" },",
"})",
"",
].join("\n"),
)

const out = await Instance.provide({
directory: tmp.path,
fn: async () => {
const out = { system: [] as string[] }
await Plugin.trigger(
"experimental.chat.system.transform",
{
model: {
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
} as any,
},
Comment on lines +97 to +102
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: this test passes a minimal { providerID, modelID } object cast as any for input.model, but the hook contract is SDK Model. Using a more realistic Model (or at least including the expected fields like model.id) will make this regression test closer to real Plugin.trigger usage and less brittle if hooks start depending on model properties.

Copilot uses AI. Check for mistakes.
out,
)
return out
},
})

expect(out.system).toEqual(["async"])
})
})
Loading