From 8d2669d8b7f5b275ba03faa7a3d42608de406291 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:19:00 +0000 Subject: [PATCH] test(cli): warm plugin import pipeline and share plugin fixture The two plugin tests in provider.test.ts that were spuriously failing on Windows CI each wrote a unique .opencode/plugin/*.ts file into a fresh tmp dir and then triggered a cold Bun TS import. Cold .ts imports on Windows can exceed 30 s which caused the test timeout. - Add a shared plugin fixture (test/fixture/plugins/config-mutator.ts) that accepts tuple options to mutate provider, enabled_providers, and disabled_providers. The two tests now reference this single file so Bun's module cache short-circuits after the first import. - Warm Bun's TS import pipeline by importing the fixture once during test/preload.ts so the first test to hit the plugin loader does not pay the full cold-start cost. Provider test suite drops from ~88 s to ~46 s locally and both plugin tests now complete well inside the 30 s per-test ceiling. --- .../test/fixture/plugins/config-mutator.ts | 26 ++++++ packages/opencode/test/preload.ts | 13 +++ .../opencode/test/provider/provider.test.ts | 91 ++++++++++--------- 3 files changed, 86 insertions(+), 44 deletions(-) create mode 100644 packages/opencode/test/fixture/plugins/config-mutator.ts diff --git a/packages/opencode/test/fixture/plugins/config-mutator.ts b/packages/opencode/test/fixture/plugins/config-mutator.ts new file mode 100644 index 00000000000..2b9b1826cc9 --- /dev/null +++ b/packages/opencode/test/fixture/plugins/config-mutator.ts @@ -0,0 +1,26 @@ +// Shared plugin fixture for tests that need to exercise real plugin.config() +// hook behavior without paying the cost of cold-transpiling a fresh .ts file +// per test. Each test references this same file URL via config.plugin with +// tuple options, so Bun's module cache short-circuits after the first import. +// +// Options schema: +// provider?: Record // merged into cfg.provider +// enabled_providers?: string[] // replaces cfg.enabled_providers +// disabled_providers?: string[] // replaces cfg.disabled_providers +export default { + id: "test.config-mutator", + server: async (_input: unknown, options: Record | undefined) => ({ + async config(cfg: Record) { + const opts = options ?? {} + if (opts.provider && typeof opts.provider === "object") { + cfg.provider = Object.assign({}, cfg.provider ?? {}, opts.provider) + } + if (Array.isArray(opts.enabled_providers)) { + cfg.enabled_providers = [...opts.enabled_providers] + } + if (Array.isArray(opts.disabled_providers)) { + cfg.disabled_providers = [...opts.disabled_providers] + } + }, + }), +} diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index f9a61caa9ad..8f8382cf712 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -89,3 +89,16 @@ void Log.init({ }) initProjectors() + +// Warm up Bun's dynamic import pipeline by loading a representative plugin +// .ts fixture once during preload. Bun keeps its module cache across tests, so +// later plugin tests that reference this same file URL hit the cache instead +// of paying the cold TS-transpile cost (which is the main source of timeouts +// in plugin tests on Windows CI). +const warm = path.join(import.meta.dir, "fixture", "plugins", "config-mutator.ts") +const { pathToFileURL } = await import("url") +await import(pathToFileURL(warm).href).catch((err) => { + // Best-effort. If warm-up fails, tests can still run -- they will just pay + // the cold-import cost on their first plugin load. + console.warn("preload: plugin warm-up failed", err) +}) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index d19ec611f64..1201e9142f7 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1,6 +1,7 @@ import { test, expect } from "bun:test" -import { mkdir, unlink } from "fs/promises" +import { unlink } from "fs/promises" import path from "path" +import { pathToFileURL } from "url" import { Global } from "../../src/global" import { Filesystem } from "../../src/util" @@ -16,6 +17,11 @@ import { AppRuntime } from "../../src/effect/app-runtime" import { makeRuntime } from "../../src/effect/run-service" import { Auth } from "../../src/auth" // kilocode_change +// Shared plugin fixture. Tests reference this same file so Bun's module cache +// short-circuits after the first import; individual test files no longer pay +// the cold TS-transpile cost. +const configMutator = pathToFileURL(path.join(import.meta.dir, "..", "fixture", "plugins", "config-mutator.ts")).href + const env = makeRuntime(Env.Service, Env.defaultLayer) const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v)) @@ -2484,33 +2490,32 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => { test("plugin config providers persist after instance dispose", async () => { await using tmp = await tmpdir({ init: async (dir) => { - const root = path.join(dir, ".opencode", "plugin") - await mkdir(root, { recursive: true }) - await Bun.write( - path.join(root, "demo-provider.ts"), - [ - "export default {", - ' id: "demo.plugin-provider",', - " server: async () => ({", - " async config(cfg) {", - " cfg.provider ??= {}", - " cfg.provider.demo = {", - ' name: "Demo Provider",', - ' npm: "@ai-sdk/openai-compatible",', - ' api: "https://example.com/v1",', - " models: {", - " chat: {", - ' name: "Demo Chat",', - " tool_call: true,", - " limit: { context: 128000, output: 4096 },", - " },", - " },", - " }", - " },", - " }),", - "}", - "", - ].join("\n"), + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://app.kilo.ai/config.json", + plugin: [ + [ + configMutator, + { + provider: { + demo: { + name: "Demo Provider", + npm: "@ai-sdk/openai-compatible", + api: "https://example.com/v1", + models: { + chat: { + name: "Demo Chat", + tool_call: true, + limit: { context: 128000, output: 4096 }, + }, + }, + }, + }, + }, + ], + ], + }), ) }, }) @@ -2543,22 +2548,20 @@ test("plugin config providers persist after instance dispose", async () => { test("plugin config enabled and disabled providers are honored", async () => { await using tmp = await tmpdir({ init: async (dir) => { - const root = path.join(dir, ".opencode", "plugin") - await mkdir(root, { recursive: true }) - await Bun.write( - path.join(root, "provider-filter.ts"), - [ - "export default {", - ' id: "demo.provider-filter",', - " server: async () => ({", - " async config(cfg) {", - ' cfg.enabled_providers = ["anthropic", "openai"]', - ' cfg.disabled_providers = ["openai"]', - " },", - " }),", - "}", - "", - ].join("\n"), + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://app.kilo.ai/config.json", + plugin: [ + [ + configMutator, + { + enabled_providers: ["anthropic", "openai"], + disabled_providers: ["openai"], + }, + ], + ], + }), ) }, })