From 80566f0def86b66f772e750bd8d828bb3e211be1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Apr 2026 12:20:33 -0400 Subject: [PATCH 1/2] refactor(config): remove async facade exports --- packages/opencode/script/seed-e2e.ts | 2 +- packages/opencode/src/cli/cmd/debug/config.ts | 3 +- packages/opencode/src/cli/cmd/mcp.ts | 9 +- packages/opencode/src/cli/cmd/providers.ts | 3 +- packages/opencode/src/cli/cmd/tui/worker.ts | 2 +- packages/opencode/src/cli/network.ts | 3 +- packages/opencode/src/cli/upgrade.ts | 2 +- packages/opencode/src/config/config.ts | 39 ----- packages/opencode/src/config/tui.ts | 3 +- .../opencode/src/server/instance/config.ts | 5 +- .../opencode/src/server/instance/global.ts | 4 +- .../opencode/src/server/instance/provider.ts | 2 +- packages/opencode/src/session/llm.ts | 5 +- .../opencode/test/config/agent-color.test.ts | 5 +- packages/opencode/test/config/config.test.ts | 142 +++++++++--------- packages/opencode/test/config/tui.test.ts | 9 +- .../opencode/test/permission-task.test.ts | 15 +- 17 files changed, 119 insertions(+), 134 deletions(-) diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts index fe83b8ec08..7010f2d96a 100644 --- a/packages/opencode/script/seed-e2e.ts +++ b/packages/opencode/script/seed-e2e.ts @@ -25,7 +25,7 @@ const seed = async () => { directory: dir, init: () => AppRuntime.runPromise(InstanceBootstrap), fn: async () => { - await Config.waitForDependencies() + await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.waitForDependencies())) await AppRuntime.runPromise( Effect.gen(function* () { const registry = yield* ToolRegistry.Service diff --git a/packages/opencode/src/cli/cmd/debug/config.ts b/packages/opencode/src/cli/cmd/debug/config.ts index af536f93ce..59e29c4a38 100644 --- a/packages/opencode/src/cli/cmd/debug/config.ts +++ b/packages/opencode/src/cli/cmd/debug/config.ts @@ -1,5 +1,6 @@ import { EOL } from "os" import { Config } from "../../../config/config" +import { AppRuntime } from "@/effect/app-runtime" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" @@ -9,7 +10,7 @@ export const ConfigCommand = cmd({ builder: (yargs) => yargs, async handler() { await bootstrap(process.cwd(), async () => { - const config = await Config.get() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) process.stdout.write(JSON.stringify(config, null, 2) + EOL) }) }, diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 41e498102e..e33ab6a35c 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -15,6 +15,7 @@ import { Global } from "../../global" import { modify, applyEdits } from "jsonc-parser" import { Filesystem } from "../../util/filesystem" import { Bus } from "../../bus" +import { AppRuntime } from "@/effect/app-runtime" function getAuthStatusIcon(status: MCP.AuthStatus): string { switch (status) { @@ -75,7 +76,7 @@ export const McpListCommand = cmd({ UI.empty() prompts.intro("MCP Servers") - const config = await Config.get() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) const mcpServers = config.mcp ?? {} const statuses = await MCP.status() @@ -152,7 +153,7 @@ export const McpAuthCommand = cmd({ UI.empty() prompts.intro("MCP OAuth Authentication") - const config = await Config.get() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) const mcpServers = config.mcp ?? {} // Get OAuth-capable servers (remote servers with oauth not explicitly disabled) @@ -289,7 +290,7 @@ export const McpAuthListCommand = cmd({ UI.empty() prompts.intro("MCP OAuth Status") - const config = await Config.get() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) const mcpServers = config.mcp ?? {} // Get OAuth-capable servers @@ -595,7 +596,7 @@ export const McpDebugCommand = cmd({ UI.empty() prompts.intro("MCP OAuth Debug") - const config = await Config.get() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) const mcpServers = config.mcp ?? {} const serverName = args.name diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 52da441904..1c5205af67 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -13,6 +13,7 @@ import { Instance } from "../../project/instance" import type { Hooks } from "@opencode-ai/plugin" import { Process } from "../../util/process" import { text } from "node:stream/consumers" +import { AppRuntime } from "@/effect/app-runtime" type PluginAuth = NonNullable @@ -311,7 +312,7 @@ export const ProvidersLoginCommand = cmd({ } await ModelsDev.refresh(true).catch(() => {}) - const config = await Config.get() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) const disabled = new Set(config.disabled_providers ?? []) const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 5e9bc62c10..4e1bdabcdd 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -81,7 +81,7 @@ export const rpc = { }) }, async reload() { - await Config.invalidate(true) + await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.invalidate(true))) }, async shutdown() { Log.Default.info("worker shutting down") diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index 84268e2675..cea49affa5 100644 --- a/packages/opencode/src/cli/network.ts +++ b/packages/opencode/src/cli/network.ts @@ -1,5 +1,6 @@ import type { Argv, InferredOptionTypes } from "yargs" import { Config } from "../config/config" +import { AppRuntime } from "@/effect/app-runtime" const options = { port: { @@ -37,7 +38,7 @@ export function withNetworkOptions(yargs: Argv) { } export async function resolveNetworkOptions(args: NetworkOptions) { - const config = await Config.getGlobal() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) const portExplicitlySet = process.argv.includes("--port") const hostnameExplicitlySet = process.argv.includes("--hostname") const mdnsExplicitlySet = process.argv.includes("--mdns") diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index e902dcb921..f67b662455 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -5,7 +5,7 @@ import { Flag } from "@/flag/flag" import { Installation } from "@/installation" export async function upgrade() { - const config = await Config.getGlobal() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {}) if (!latest) return diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ab3abaf94e..6aa79e3090 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -33,7 +33,6 @@ import { ConfigPaths } from "./paths" import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@/filesystem" import { InstanceState } from "@/effect/instance-state" -import { makeRuntime } from "@/effect/run-service" import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" import { Flock } from "@/util/flock" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" @@ -1661,42 +1660,4 @@ export namespace Config { Layer.provide(Auth.defaultLayer), Layer.provide(Account.defaultLayer), ) - - const { runPromise } = makeRuntime(Service, defaultLayer) - - export async function get() { - return runPromise((svc) => svc.get()) - } - - export async function getGlobal() { - return runPromise((svc) => svc.getGlobal()) - } - - export async function getConsoleState() { - return runPromise((svc) => svc.getConsoleState()) - } - - export async function installDependencies(dir: string, input?: InstallInput) { - return runPromise((svc) => svc.installDependencies(dir, input)) - } - - export async function update(config: Info) { - return runPromise((svc) => svc.update(config)) - } - - export async function updateGlobal(config: Info) { - return runPromise((svc) => svc.updateGlobal(config)) - } - - export async function invalidate(wait = false) { - return runPromise((svc) => svc.invalidate(wait)) - } - - export async function directories() { - return runPromise((svc) => svc.directories()) - } - - export async function waitForDependencies() { - return runPromise((svc) => svc.waitForDependencies()) - } } diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index fa2022482d..ed9bb5b8d7 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -10,6 +10,7 @@ import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { isRecord } from "@/util/record" import { Global } from "@/global" +import { AppRuntime } from "@/effect/app-runtime" export namespace TuiConfig { const log = Log.create({ service: "tui.config" }) @@ -51,7 +52,7 @@ export namespace TuiConfig { } function installDeps(dir: string): Promise { - return Config.installDependencies(dir) + return AppRuntime.runPromise(Config.Service.use((cfg) => cfg.installDependencies(dir))) } async function mergeFile(acc: Acc, file: string) { diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index 85d28f6aa6..1ff2dfadc6 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/instance/config.ts @@ -3,6 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { Config } from "../../config/config" import { Provider } from "../../provider/provider" +import { AppRuntime } from "@/effect/app-runtime" import { mapValues } from "remeda" import { errors } from "../error" import { Log } from "../../util/log" @@ -30,7 +31,7 @@ export const ConfigRoutes = lazy(() => }, }), async (c) => { - return c.json(await Config.get()) + return c.json(await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))) }, ) .patch( @@ -54,7 +55,7 @@ export const ConfigRoutes = lazy(() => validator("json", Config.Info), async (c) => { const config = c.req.valid("json") - await Config.update(config) + await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.update(config))) return c.json(config) }, ) diff --git a/packages/opencode/src/server/instance/global.ts b/packages/opencode/src/server/instance/global.ts index 6b0a9a164b..daecb6bd36 100644 --- a/packages/opencode/src/server/instance/global.ts +++ b/packages/opencode/src/server/instance/global.ts @@ -199,7 +199,7 @@ export const GlobalRoutes = lazy(() => }, }), async (c) => { - return c.json(await Config.getGlobal()) + return c.json(await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal()))) }, ) .patch( @@ -223,7 +223,7 @@ export const GlobalRoutes = lazy(() => validator("json", Config.Info), async (c) => { const config = c.req.valid("json") - const next = await Config.updateGlobal(config) + const next = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config))) return c.json(next) }, ) diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index efd126ea0f..e0e9a9bf5e 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/instance/provider.ts @@ -40,7 +40,7 @@ export const ProviderRoutes = lazy(() => }, }), async (c) => { - const config = await Config.get() + const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) const disabled = new Set(config.disabled_providers ?? []) const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index f6e5c9a3f2..dfa50a68e4 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -21,6 +21,7 @@ import { Wildcard } from "@/util/wildcard" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { Installation } from "@/installation" +import { AppRuntime } from "@/effect/app-runtime" export namespace LLM { const log = Log.create({ service: "llm" }) @@ -94,9 +95,9 @@ export namespace LLM { modelID: input.model.id, providerID: input.model.providerID, }) - const [language, cfg, provider, auth] = await Promise.all([ + const cfg = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())) + const [language, provider, auth] = await Promise.all([ Provider.getLanguage(input.model), - Config.get(), Provider.getProvider(input.model.providerID), Auth.get(input.model.providerID), ]) diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts index b9c7cccc48..0dc2653c2d 100644 --- a/packages/opencode/test/config/agent-color.test.ts +++ b/packages/opencode/test/config/agent-color.test.ts @@ -5,6 +5,9 @@ import { Instance } from "../../src/project/instance" import { Config } from "../../src/config/config" import { Agent as AgentSvc } from "../../src/agent/agent" import { Color } from "../../src/util/color" +import { AppRuntime } from "../../src/effect/app-runtime" + +const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get())) test("agent color parsed from project config", async () => { await using tmp = await tmpdir({ @@ -24,7 +27,7 @@ test("agent color parsed from project config", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const cfg = await Config.get() + const cfg = await load() expect(cfg.agent?.["build"]?.color).toBe("#FFA500") expect(cfg.agent?.["plan"]?.color).toBe("primary") }, diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index d6931975c1..295d995fc3 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -33,15 +33,23 @@ const emptyAuth = Layer.mock(Auth.Service)({ all: () => Effect.succeed({}), }) -const it = testEffect( - Config.layer.pipe( - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(emptyAuth), - Layer.provide(emptyAccount), - Layer.provideMerge(infra), - ), +const layer = Config.layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(emptyAuth), + Layer.provide(emptyAccount), + Layer.provideMerge(infra), ) +const it = testEffect(layer) + +const run = (eff: Effect.Effect) => Effect.runPromise(eff.pipe(Effect.scoped, Effect.provide(layer))) + +const load = () => run(Config.Service.use((svc) => svc.get())) +const save = (config: Config.Info) => run(Config.Service.use((svc) => svc.update(config))) +const clear = (wait = false) => run(Config.Service.use((svc) => svc.invalidate(wait))) +const listDirs = () => run(Config.Service.use((svc) => svc.directories())) +const ready = () => run(Config.Service.use((svc) => svc.waitForDependencies())) + const installDeps = (dir: string, input?: Config.InstallInput) => Config.Service.use((svc) => svc.installDependencies(dir, input)) @@ -49,12 +57,12 @@ const installDeps = (dir: string, input?: Config.InstallInput) => const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! beforeEach(async () => { - await Config.invalidate(true) + await clear(true) }) afterEach(async () => { await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {}) - await Config.invalidate(true) + await clear(true) }) async function writeManagedSettings(settings: object, filename = "opencode.json") { @@ -72,7 +80,7 @@ async function check(map: (dir: string) => string) { await using tmp = await tmpdir({ git: true, config: { snapshot: true } }) const prev = Global.Path.config ;(Global.Path as { config: string }).config = globalTmp.path - await Config.invalidate() + await clear() try { await writeConfig(globalTmp.path, { $schema: "https://opencode.ai/config.json", @@ -81,7 +89,7 @@ async function check(map: (dir: string) => string) { await Instance.provide({ directory: map(tmp.path), fn: async () => { - const cfg = await Config.get() + const cfg = await load() expect(cfg.snapshot).toBe(true) expect(Instance.directory).toBe(Filesystem.resolve(tmp.path)) expect(Instance.project.id).not.toBe(ProjectID.global) @@ -90,7 +98,7 @@ async function check(map: (dir: string) => string) { } finally { await Instance.disposeAll() ;(Global.Path as { config: string }).config = prev - await Config.invalidate() + await clear() } } @@ -99,7 +107,7 @@ test("loads config with defaults when no files exist", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.username).toBeDefined() }, }) @@ -118,7 +126,7 @@ test("loads JSON config file", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.model).toBe("test/model") expect(config.username).toBe("testuser") }, @@ -156,7 +164,7 @@ test("ignores legacy tui keys in opencode config", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.model).toBe("test/model") expect((config as Record).theme).toBeUndefined() expect((config as Record).tui).toBeUndefined() @@ -181,7 +189,7 @@ test("loads JSONC config file", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.model).toBe("test/model") expect(config.username).toBe("testuser") }, @@ -209,7 +217,7 @@ test("jsonc overrides json in the same directory", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.model).toBe("base") expect(config.username).toBe("base") }, @@ -232,7 +240,7 @@ test("handles environment variable substitution", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.username).toBe("test-user") }, }) @@ -264,7 +272,7 @@ test("preserves env variables when adding $schema to config", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.username).toBe("secret_value") // Read the file to verify the env variable was preserved @@ -358,7 +366,7 @@ test("handles file inclusion substitution", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.username).toBe("test-user") }, }) @@ -377,7 +385,7 @@ test("handles file inclusion with replacement tokens", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.username).toBe("const out = await Bun.$`echo hi`") }, }) @@ -396,7 +404,7 @@ test("validates config schema and throws on invalid fields", async () => { directory: tmp.path, fn: async () => { // Strict schema should throw an error for invalid fields - await expect(Config.get()).rejects.toThrow() + await expect(load()).rejects.toThrow() }, }) }) @@ -410,7 +418,7 @@ test("throws error for invalid JSON", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await expect(Config.get()).rejects.toThrow() + await expect(load()).rejects.toThrow() }, }) }) @@ -433,7 +441,7 @@ test("handles agent configuration", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test_agent"]).toEqual( expect.objectContaining({ model: "test/model", @@ -464,7 +472,7 @@ test("treats agent variant as model-scoped setting (not provider option)", async await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const agent = config.agent?.["test_agent"] expect(agent?.variant).toBe("xhigh") @@ -494,7 +502,7 @@ test("handles command configuration", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.command?.["test_command"]).toEqual({ template: "test template", description: "test command", @@ -519,7 +527,7 @@ test("migrates autoshare to share field", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.share).toBe("auto") expect(config.autoshare).toBe(true) }, @@ -546,7 +554,7 @@ test("migrates mode field to agent field", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test_mode"]).toEqual({ model: "test/model", temperature: 0.5, @@ -578,7 +586,7 @@ Test agent prompt`, await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]).toEqual( expect.objectContaining({ name: "test", @@ -622,7 +630,7 @@ Nested agent prompt`, await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["helper"]).toMatchObject({ name: "helper", @@ -671,7 +679,7 @@ Nested command template`, await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.command?.["hello"]).toEqual({ description: "Test command", @@ -716,7 +724,7 @@ Nested command template`, await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.command?.["hello"]).toEqual({ description: "Test command", @@ -737,7 +745,7 @@ test("updates config and writes to file", async () => { directory: tmp.path, fn: async () => { const newConfig = { model: "updated/model" } - await Config.update(newConfig as any) + await save(newConfig as any) const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json")) expect(writtenConfig.model).toBe("updated/model") @@ -750,7 +758,7 @@ test("gets config directories", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const dirs = await Config.directories() + const dirs = await listDirs() expect(dirs.length).toBeGreaterThanOrEqual(1) }, }) @@ -780,7 +788,7 @@ test("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR", as await Instance.provide({ directory: tmp.path, fn: async () => { - await Config.get() + await load() }, }) } finally { @@ -814,8 +822,8 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await Config.get() - await Config.waitForDependencies() + await load() + await ready() }, }) @@ -996,7 +1004,7 @@ test("resolves scoped npm plugins in config", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const pluginEntries = config.plugin ?? [] expect(pluginEntries).toContain("@scope/plugin") }, @@ -1034,7 +1042,7 @@ test("merges plugin arrays from global and local configs", async () => { await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { - const config = await Config.get() + const config = await load() const plugins = config.plugin ?? [] // Should contain both global and local plugins @@ -1070,7 +1078,7 @@ Helper subagent prompt`, await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["helper"]).toMatchObject({ name: "helper", model: "test/model", @@ -1109,7 +1117,7 @@ test("merges instructions arrays from global and local configs", async () => { await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { - const config = await Config.get() + const config = await load() const instructions = config.instructions ?? [] expect(instructions).toContain("global-instructions.md") @@ -1148,7 +1156,7 @@ test("deduplicates duplicate instructions from global and local configs", async await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { - const config = await Config.get() + const config = await load() const instructions = config.instructions ?? [] expect(instructions).toContain("global-only.md") @@ -1193,7 +1201,7 @@ test("deduplicates duplicate plugins from global and local configs", async () => await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { - const config = await Config.get() + const config = await load() const plugins = config.plugin ?? [] // Should contain all unique plugins @@ -1242,7 +1250,7 @@ test("keeps plugin origins aligned with merged plugin list", async () => { await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { - const cfg = await Config.get() + const cfg = await load() const plugins = cfg.plugin ?? [] const origins = cfg.plugin_origins ?? [] const names = plugins.map((item) => Config.pluginSpecifier(item)) @@ -1283,7 +1291,7 @@ test("migrates legacy tools config to permissions - allow", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ bash: "allow", read: "allow", @@ -1314,7 +1322,7 @@ test("migrates legacy tools config to permissions - deny", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ bash: "deny", webfetch: "deny", @@ -1344,7 +1352,7 @@ test("migrates legacy write tool to edit permission", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow", }) @@ -1376,7 +1384,7 @@ test("managed settings override user settings", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.model).toBe("managed/model") expect(config.share).toBe("disabled") expect(config.username).toBe("testuser") @@ -1404,7 +1412,7 @@ test("managed settings override project settings", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.autoupdate).toBe(false) expect(config.disabled_providers).toEqual(["openai"]) }, @@ -1424,7 +1432,7 @@ test("missing managed settings file is not an error", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.model).toBe("user/model") }, }) @@ -1451,7 +1459,7 @@ test("migrates legacy edit tool to edit permission", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny", }) @@ -1480,7 +1488,7 @@ test("migrates legacy patch tool to edit permission", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow", }) @@ -1509,7 +1517,7 @@ test("migrates legacy multiedit tool to edit permission", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny", }) @@ -1541,7 +1549,7 @@ test("migrates mixed legacy tools config", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ bash: "allow", edit: "allow", @@ -1576,7 +1584,7 @@ test("merges legacy tools with existing permission config", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.agent?.["test"]?.permission).toEqual({ glob: "allow", bash: "allow", @@ -1611,7 +1619,7 @@ test("permission config preserves key order", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(Object.keys(config.permission!)).toEqual([ "*", "edit", @@ -1671,7 +1679,7 @@ test("project config can override MCP server enabled status", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() // jira should be enabled (overridden by project config) expect(config.mcp?.jira).toEqual({ type: "remote", @@ -1727,7 +1735,7 @@ test("MCP config deep merges preserving base config properties", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.mcp?.myserver).toEqual({ type: "remote", url: "https://myserver.example.com/mcp", @@ -1778,7 +1786,7 @@ test("local .opencode config can override MCP from project config", async () => await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.mcp?.docs?.enabled).toBe(true) }, }) @@ -2029,7 +2037,7 @@ describe("deduplicatePluginOrigins", () => { await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { - const config = await Config.get() + const config = await load() const plugins = config.plugin ?? [] expect(plugins.some((p) => Config.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true) @@ -2061,7 +2069,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() // Project config should NOT be loaded - model should be default, not "project/model" expect(config.model).not.toBe("project/model") expect(config.username).not.toBe("project-user") @@ -2092,7 +2100,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const directories = await Config.directories() + const directories = await listDirs() // Project .opencode should NOT be in directories list const hasProjectOpencode = directories.some((d) => d.startsWith(tmp.path)) expect(hasProjectOpencode).toBe(false) @@ -2117,7 +2125,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { directory: tmp.path, fn: async () => { // Should still get default config (from global or defaults) - const config = await Config.get() + const config = await load() expect(config).toBeDefined() expect(config.username).toBeDefined() }, @@ -2160,7 +2168,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { fn: async () => { // The relative instruction should be skipped without error // We're mainly verifying this doesn't throw and the config loads - const config = await Config.get() + const config = await load() expect(config).toBeDefined() // The instruction should have been skipped (warning logged) // We can't easily test the warning was logged, but we verify @@ -2218,7 +2226,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { await Instance.provide({ directory: projectTmp.path, fn: async () => { - const config = await Config.get() + const config = await load() // Should load from OPENCODE_CONFIG_DIR, not project expect(config.model).toBe("configdir/model") }, @@ -2253,7 +2261,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.username).toBe("test_api_key_12345") }, }) @@ -2287,7 +2295,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(config.username).toBe("secret_key_from_file") }, }) diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index b761d59ea4..529d88bce1 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -7,12 +7,15 @@ import { Config } from "../../src/config/config" import { TuiConfig } from "../../src/config/tui" import { Global } from "../../src/global" import { Filesystem } from "../../src/util/filesystem" +import { AppRuntime } from "../../src/effect/app-runtime" const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! const wintest = process.platform === "win32" ? test : test.skip +const clear = (wait = false) => AppRuntime.runPromise(Config.Service.use((svc) => svc.invalidate(wait))) +const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get())) beforeEach(async () => { - await Config.invalidate(true) + await clear(true) }) afterEach(async () => { @@ -23,7 +26,7 @@ afterEach(async () => { await fs.rm(path.join(Global.Path.config, "tui.json"), { force: true }).catch(() => {}) await fs.rm(path.join(Global.Path.config, "tui.jsonc"), { force: true }).catch(() => {}) await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {}) - await Config.invalidate(true) + await clear(true) }) test("keeps server and tui plugin merge semantics aligned", async () => { @@ -79,7 +82,7 @@ test("keeps server and tui plugin merge semantics aligned", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const server = await Config.get() + const server = await load() const tui = await TuiConfig.get() const serverPlugins = (server.plugin ?? []).map((item) => Config.pluginSpecifier(item)) const tuiPlugins = (tui.plugin ?? []).map((item) => Config.pluginSpecifier(item)) diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index 3ca32bf414..d415d23ebc 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/packages/opencode/test/permission-task.test.ts @@ -3,6 +3,9 @@ import { Permission } from "../src/permission" import { Config } from "../src/config/config" import { Instance } from "../src/project/instance" import { tmpdir } from "./fixture/fixture" +import { AppRuntime } from "../src/effect/app-runtime" + +const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get())) afterEach(async () => { await Instance.disposeAll() @@ -158,7 +161,7 @@ describe("permission.task with real config files", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const ruleset = Permission.fromConfig(config.permission ?? {}) // general and orchestrator-fast should be allowed, code-reviewer denied expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow") @@ -183,7 +186,7 @@ describe("permission.task with real config files", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const ruleset = Permission.fromConfig(config.permission ?? {}) // general and code-reviewer should be ask, orchestrator-* denied expect(Permission.evaluate("task", "general", ruleset).action).toBe("ask") @@ -208,7 +211,7 @@ describe("permission.task with real config files", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const ruleset = Permission.fromConfig(config.permission ?? {}) expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow") expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") @@ -235,7 +238,7 @@ describe("permission.task with real config files", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const ruleset = Permission.fromConfig(config.permission ?? {}) // Verify task permissions @@ -273,7 +276,7 @@ describe("permission.task with real config files", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const ruleset = Permission.fromConfig(config.permission ?? {}) // Last matching rule wins - "*" deny is last, so all agents are denied @@ -304,7 +307,7 @@ describe("permission.task with real config files", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() const ruleset = Permission.fromConfig(config.permission ?? {}) // Evaluate uses findLast - "general" allow comes after "*" deny From 591e197c4657e5c92135693cb2dccaa5b15e5090 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Apr 2026 13:14:57 -0400 Subject: [PATCH 2/2] test(config): avoid app runtime in config tests --- packages/opencode/test/config/tui.test.ts | 7 ++++--- packages/opencode/test/permission-task.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index 529d88bce1..e541662350 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, expect, test } from "bun:test" +import { Effect } from "effect" import path from "path" import fs from "fs/promises" import { tmpdir } from "../fixture/fixture" @@ -7,12 +8,12 @@ import { Config } from "../../src/config/config" import { TuiConfig } from "../../src/config/tui" import { Global } from "../../src/global" import { Filesystem } from "../../src/util/filesystem" -import { AppRuntime } from "../../src/effect/app-runtime" const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! const wintest = process.platform === "win32" ? test : test.skip -const clear = (wait = false) => AppRuntime.runPromise(Config.Service.use((svc) => svc.invalidate(wait))) -const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get())) +const clear = (wait = false) => + Effect.runPromise(Config.Service.use((svc) => svc.invalidate(wait)).pipe(Effect.provide(Config.defaultLayer))) +const load = () => Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.provide(Config.defaultLayer))) beforeEach(async () => { await clear(true) diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index d415d23ebc..8812fc6ac3 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/packages/opencode/test/permission-task.test.ts @@ -3,9 +3,9 @@ import { Permission } from "../src/permission" import { Config } from "../src/config/config" import { Instance } from "../src/project/instance" import { tmpdir } from "./fixture/fixture" -import { AppRuntime } from "../src/effect/app-runtime" +import { Effect } from "effect" -const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get())) +const load = () => Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.provide(Config.defaultLayer))) afterEach(async () => { await Instance.disposeAll()