import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:test" import { Effect, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config } from "../../src/config/config" import { Instance } from "../../src/project/instance" import { Auth } from "../../src/auth" import { AccessToken, Account, AccountID, OrgID } from "../../src/account" import { AppFileSystem } from "../../src/filesystem" import { provideTmpdirInstance } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" /** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */ const infra = CrossSpawnSpawner.defaultLayer.pipe( Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)), ) import path from "path" import fs from "fs/promises" import { pathToFileURL } from "url" import { Global } from "../../src/global" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util/filesystem" import * as Network from "../../src/util/network" import { Npm } from "../../src/npm" const emptyAccount = Layer.mock(Account.Service)({ active: () => Effect.succeed(Option.none()), }) const emptyAuth = Layer.mock(Auth.Service)({ all: () => Effect.succeed({}), }) // Get managed config directory from environment (set in preload.ts) const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! beforeEach(async () => { await Config.invalidate(true) }) afterEach(async () => { await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {}) await Config.invalidate(true) }) async function writeManagedSettings(settings: object, filename = "opencode.json") { await fs.mkdir(managedConfigDir, { recursive: true }) await Filesystem.write(path.join(managedConfigDir, filename), JSON.stringify(settings)) } async function writeConfig(dir: string, config: object, name = "opencode.json") { await Filesystem.write(path.join(dir, name), JSON.stringify(config)) } async function check(map: (dir: string) => string) { if (process.platform !== "win32") return await using globalTmp = await tmpdir() 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() try { await writeConfig(globalTmp.path, { $schema: "https://opencode.ai/config.json", snapshot: false, }) await Instance.provide({ directory: map(tmp.path), fn: async () => { const cfg = await Config.get() expect(cfg.snapshot).toBe(true) expect(Instance.directory).toBe(Filesystem.resolve(tmp.path)) expect(Instance.project.id).not.toBe(ProjectID.global) }, }) } finally { await Instance.disposeAll() ;(Global.Path as { config: string }).config = prev await Config.invalidate() } } test("loads config with defaults when no files exist", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.username).toBeDefined() }, }) }) test("loads JSON config file", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", model: "test/model", username: "testuser", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.model).toBe("test/model") expect(config.username).toBe("testuser") }, }) }) test("loads project config from Git Bash and MSYS2 paths on Windows", async () => { // Git Bash and MSYS2 both use //... paths on Windows. await check((dir) => { const drive = dir[0].toLowerCase() const rest = dir.slice(2).replaceAll("\\", "/") return `/${drive}${rest}` }) }) test("loads project config from Cygwin paths on Windows", async () => { await check((dir) => { const drive = dir[0].toLowerCase() const rest = dir.slice(2).replaceAll("\\", "/") return `/cygdrive/${drive}${rest}` }) }) test("ignores legacy tui keys in opencode config", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", model: "test/model", theme: "legacy", tui: { scroll_speed: 4 }, }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.model).toBe("test/model") expect((config as Record).theme).toBeUndefined() expect((config as Record).tui).toBeUndefined() }, }) }) test("loads JSONC config file", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.jsonc"), `{ // This is a comment "$schema": "https://opencode.ai/config.json", "model": "test/model", "username": "testuser" }`, ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.model).toBe("test/model") expect(config.username).toBe("testuser") }, }) }) test("jsonc overrides json in the same directory", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig( dir, { $schema: "https://opencode.ai/config.json", model: "base", username: "base", }, "opencode.jsonc", ) await writeConfig(dir, { $schema: "https://opencode.ai/config.json", model: "override", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.model).toBe("base") expect(config.username).toBe("base") }, }) }) test("handles environment variable substitution", async () => { const originalEnv = process.env["TEST_VAR"] process.env["TEST_VAR"] = "test-user" try { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", username: "{env:TEST_VAR}", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.username).toBe("test-user") }, }) } finally { if (originalEnv !== undefined) { process.env["TEST_VAR"] = originalEnv } else { delete process.env["TEST_VAR"] } } }) test("preserves env variables when adding $schema to config", async () => { const originalEnv = process.env["PRESERVE_VAR"] process.env["PRESERVE_VAR"] = "secret_value" try { await using tmp = await tmpdir({ init: async (dir) => { // Config without $schema - should trigger auto-add await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ username: "{env:PRESERVE_VAR}", }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.username).toBe("secret_value") // Read the file to verify the env variable was preserved const content = await Filesystem.readText(path.join(tmp.path, "opencode.json")) expect(content).toContain("{env:PRESERVE_VAR}") expect(content).not.toContain("secret_value") expect(content).toContain("$schema") }, }) } finally { if (originalEnv !== undefined) { process.env["PRESERVE_VAR"] = originalEnv } else { delete process.env["PRESERVE_VAR"] } } }) test("resolves env templates in account config with account token", async () => { const originalControlToken = process.env["OPENCODE_CONSOLE_TOKEN"] const fakeAccount = Layer.mock(Account.Service)({ active: () => Effect.succeed( Option.some({ id: AccountID.make("account-1"), email: "user@example.com", url: "https://control.example.com", active_org_id: OrgID.make("org-1"), }), ), config: () => Effect.succeed( Option.some({ provider: { opencode: { options: { apiKey: "{env:OPENCODE_CONSOLE_TOKEN}" } } }, }), ), token: () => Effect.succeed(Option.some(AccessToken.make("st_test_token"))), }) const layer = Config.layer.pipe( Layer.provide(AppFileSystem.defaultLayer), Layer.provide(emptyAuth), Layer.provide(fakeAccount), Layer.provideMerge(infra), ) try { await provideTmpdirInstance(() => Config.Service.use((svc) => Effect.gen(function* () { const config = yield* svc.get() expect(config.provider?.["opencode"]?.options?.apiKey).toBe("st_test_token") }), ), ).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise) } finally { if (originalControlToken !== undefined) { process.env["OPENCODE_CONSOLE_TOKEN"] = originalControlToken } else { delete process.env["OPENCODE_CONSOLE_TOKEN"] } } }) test("handles file inclusion substitution", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write(path.join(dir, "included.txt"), "test-user") await writeConfig(dir, { $schema: "https://opencode.ai/config.json", username: "{file:included.txt}", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.username).toBe("test-user") }, }) }) test("handles file inclusion with replacement tokens", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write(path.join(dir, "included.md"), "const out = await Bun.$`echo hi`") await writeConfig(dir, { $schema: "https://opencode.ai/config.json", username: "{file:included.md}", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.username).toBe("const out = await Bun.$`echo hi`") }, }) }) test("validates config schema and throws on invalid fields", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", invalid_field: "should cause error", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { // Strict schema should throw an error for invalid fields await expect(Config.get()).rejects.toThrow() }, }) }) test("throws error for invalid JSON", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write(path.join(dir, "opencode.json"), "{ invalid json }") }, }) await Instance.provide({ directory: tmp.path, fn: async () => { await expect(Config.get()).rejects.toThrow() }, }) }) test("handles agent configuration", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", agent: { test_agent: { model: "test/model", temperature: 0.7, description: "test agent", }, }, }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test_agent"]).toEqual( expect.objectContaining({ model: "test/model", temperature: 0.7, description: "test agent", }), ) }, }) }) test("treats agent variant as model-scoped setting (not provider option)", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", agent: { test_agent: { model: "openai/gpt-5.2", variant: "xhigh", max_tokens: 123, }, }, }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() const agent = config.agent?.["test_agent"] expect(agent?.variant).toBe("xhigh") expect(agent?.options).toMatchObject({ max_tokens: 123, }) expect(agent?.options).not.toHaveProperty("variant") }, }) }) test("handles command configuration", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", command: { test_command: { template: "test template", description: "test command", agent: "test_agent", }, }, }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.command?.["test_command"]).toEqual({ template: "test template", description: "test command", agent: "test_agent", }) }, }) }) test("migrates autoshare to share field", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", autoshare: true, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.share).toBe("auto") expect(config.autoshare).toBe(true) }, }) }) test("migrates mode field to agent field", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", mode: { test_mode: { model: "test/model", temperature: 0.5, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test_mode"]).toEqual({ model: "test/model", temperature: 0.5, mode: "primary", options: {}, permission: {}, }) }, }) }) test("loads config from .opencode directory", async () => { await using tmp = await tmpdir({ init: async (dir) => { const opencodeDir = path.join(dir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) const agentDir = path.join(opencodeDir, "agent") await fs.mkdir(agentDir, { recursive: true }) await Filesystem.write( path.join(agentDir, "test.md"), `--- model: test/model --- Test agent prompt`, ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]).toEqual( expect.objectContaining({ name: "test", model: "test/model", prompt: "Test agent prompt", }), ) }, }) }) test("loads agents from .opencode/agents (plural)", async () => { await using tmp = await tmpdir({ init: async (dir) => { const opencodeDir = path.join(dir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) const agentsDir = path.join(opencodeDir, "agents") await fs.mkdir(path.join(agentsDir, "nested"), { recursive: true }) await Filesystem.write( path.join(agentsDir, "helper.md"), `--- model: test/model mode: subagent --- Helper agent prompt`, ) await Filesystem.write( path.join(agentsDir, "nested", "child.md"), `--- model: test/model mode: subagent --- Nested agent prompt`, ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["helper"]).toMatchObject({ name: "helper", model: "test/model", mode: "subagent", prompt: "Helper agent prompt", }) expect(config.agent?.["nested/child"]).toMatchObject({ name: "nested/child", model: "test/model", mode: "subagent", prompt: "Nested agent prompt", }) }, }) }) test("loads commands from .opencode/command (singular)", async () => { await using tmp = await tmpdir({ init: async (dir) => { const opencodeDir = path.join(dir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) const commandDir = path.join(opencodeDir, "command") await fs.mkdir(path.join(commandDir, "nested"), { recursive: true }) await Filesystem.write( path.join(commandDir, "hello.md"), `--- description: Test command --- Hello from singular command`, ) await Filesystem.write( path.join(commandDir, "nested", "child.md"), `--- description: Nested command --- Nested command template`, ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.command?.["hello"]).toEqual({ description: "Test command", template: "Hello from singular command", }) expect(config.command?.["nested/child"]).toEqual({ description: "Nested command", template: "Nested command template", }) }, }) }) test("loads commands from .opencode/commands (plural)", async () => { await using tmp = await tmpdir({ init: async (dir) => { const opencodeDir = path.join(dir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) const commandsDir = path.join(opencodeDir, "commands") await fs.mkdir(path.join(commandsDir, "nested"), { recursive: true }) await Filesystem.write( path.join(commandsDir, "hello.md"), `--- description: Test command --- Hello from plural commands`, ) await Filesystem.write( path.join(commandsDir, "nested", "child.md"), `--- description: Nested command --- Nested command template`, ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.command?.["hello"]).toEqual({ description: "Test command", template: "Hello from plural commands", }) expect(config.command?.["nested/child"]).toEqual({ description: "Nested command", template: "Nested command template", }) }, }) }) test("updates config and writes to file", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const newConfig = { model: "updated/model" } await Config.update(newConfig as any) const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json")) expect(writtenConfig.model).toBe("updated/model") }, }) }) test("gets config directories", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const dirs = await Config.directories() expect(dirs.length).toBeGreaterThanOrEqual(1) }, }) }) test("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR", async () => { if (process.platform === "win32") return await using tmp = await tmpdir({ init: async (dir) => { const ro = path.join(dir, "readonly") await fs.mkdir(ro, { recursive: true }) await fs.chmod(ro, 0o555) return ro }, dispose: async (dir) => { const ro = path.join(dir, "readonly") await fs.chmod(ro, 0o755).catch(() => {}) return ro }, }) const prev = process.env.OPENCODE_CONFIG_DIR process.env.OPENCODE_CONFIG_DIR = tmp.extra try { await Instance.provide({ directory: tmp.path, fn: async () => { await Config.get() }, }) } finally { if (prev === undefined) delete process.env.OPENCODE_CONFIG_DIR else process.env.OPENCODE_CONFIG_DIR = prev } }) test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => { await using tmp = await tmpdir({ init: async (dir) => { const cfg = path.join(dir, "configdir") await fs.mkdir(cfg, { recursive: true }) return cfg }, }) const prev = process.env.OPENCODE_CONFIG_DIR process.env.OPENCODE_CONFIG_DIR = tmp.extra const online = spyOn(Network, "online").mockReturnValue(false) const install = spyOn(Npm, "install").mockImplementation(async (dir: string) => { const mod = path.join(dir, "node_modules", "@opencode-ai", "plugin") await fs.mkdir(mod, { recursive: true }) await Filesystem.write( path.join(mod, "package.json"), JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }), ) }) try { await Instance.provide({ directory: tmp.path, fn: async () => { await Config.get() await Config.waitForDependencies() }, }) expect(await Filesystem.exists(path.join(tmp.extra, "package.json"))).toBe(true) expect(await Filesystem.exists(path.join(tmp.extra, ".gitignore"))).toBe(true) expect(await Filesystem.readText(path.join(tmp.extra, ".gitignore"))).toContain("package-lock.json") } finally { online.mockRestore() install.mockRestore() if (prev === undefined) delete process.env.OPENCODE_CONFIG_DIR else process.env.OPENCODE_CONFIG_DIR = prev } }) test("dedupes concurrent config dependency installs for the same dir", async () => { await using tmp = await tmpdir() const dir = path.join(tmp.path, "a") await fs.mkdir(dir, { recursive: true }) const ticks: number[] = [] let calls = 0 let start = () => {} let done = () => {} let blocked = () => {} const ready = new Promise((resolve) => { start = resolve }) const gate = new Promise((resolve) => { done = resolve }) const waiting = new Promise((resolve) => { blocked = resolve }) const online = spyOn(Network, "online").mockReturnValue(false) const targetDir = dir const run = spyOn(Npm, "install").mockImplementation(async (d: string) => { const hit = path.normalize(d) === path.normalize(targetDir) if (hit) { calls += 1 start() await gate } const mod = path.join(d, "node_modules", "@opencode-ai", "plugin") await fs.mkdir(mod, { recursive: true }) await Filesystem.write( path.join(mod, "package.json"), JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }), ) if (hit) { start() await gate } }) try { const first = Config.installDependencies(dir) await ready const second = Config.installDependencies(dir, { waitTick: (tick) => { ticks.push(tick.attempt) blocked() blocked = () => {} }, }) await waiting done() await Promise.all([first, second]) } finally { online.mockRestore() run.mockRestore() } expect(calls).toBe(2) expect(ticks.length).toBeGreaterThan(0) expect(await Filesystem.exists(path.join(dir, "package.json"))).toBe(true) }) test("serializes config dependency installs across dirs", async () => { if (process.platform !== "win32") return await using tmp = await tmpdir() const a = path.join(tmp.path, "a") const b = path.join(tmp.path, "b") await fs.mkdir(a, { recursive: true }) await fs.mkdir(b, { recursive: true }) let calls = 0 let open = 0 let peak = 0 let start = () => {} let done = () => {} const ready = new Promise((resolve) => { start = resolve }) const gate = new Promise((resolve) => { done = resolve }) const online = spyOn(Network, "online").mockReturnValue(false) const run = spyOn(Npm, "install").mockImplementation(async (dir: string) => { const cwd = path.normalize(dir) const hit = cwd === path.normalize(a) || cwd === path.normalize(b) if (hit) { calls += 1 open += 1 peak = Math.max(peak, open) if (calls === 1) { start() await gate } } const mod = path.join(cwd, "node_modules", "@opencode-ai", "plugin") await fs.mkdir(mod, { recursive: true }) await Filesystem.write( path.join(mod, "package.json"), JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }), ) if (hit) { open -= 1 } }) try { const first = Config.installDependencies(a) await ready const second = Config.installDependencies(b) done() await Promise.all([first, second]) } finally { online.mockRestore() run.mockRestore() } expect(calls).toBe(2) expect(peak).toBe(1) }) test("resolves scoped npm plugins in config", async () => { await using tmp = await tmpdir({ init: async (dir) => { const pluginDir = path.join(dir, "node_modules", "@scope", "plugin") await fs.mkdir(pluginDir, { recursive: true }) await Filesystem.write( path.join(dir, "package.json"), JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2), ) await Filesystem.write( path.join(pluginDir, "package.json"), JSON.stringify( { name: "@scope/plugin", version: "1.0.0", type: "module", main: "./index.js", }, null, 2, ), ) await Filesystem.write(path.join(pluginDir, "index.js"), "export default {}\n") await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] }, null, 2), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() const pluginEntries = config.plugin ?? [] expect(pluginEntries).toContain("@scope/plugin") }, }) }) test("merges plugin arrays from global and local configs", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Create a nested project structure with local .opencode config const projectDir = path.join(dir, "project") const opencodeDir = path.join(projectDir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) // Global config with plugins await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["global-plugin-1", "global-plugin-2"], }), ) // Local .opencode config with different plugins await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["local-plugin-1"], }), ) }, }) await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { const config = await Config.get() const plugins = config.plugin ?? [] // Should contain both global and local plugins expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) expect(plugins.some((p) => p.includes("global-plugin-2"))).toBe(true) expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) // Should have all 3 plugins (not replaced, but merged) const pluginNames = plugins.filter((p) => p.includes("global-plugin") || p.includes("local-plugin")) expect(pluginNames.length).toBeGreaterThanOrEqual(3) }, }) }) test("does not error when only custom agent is a subagent", async () => { await using tmp = await tmpdir({ init: async (dir) => { const opencodeDir = path.join(dir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) const agentDir = path.join(opencodeDir, "agent") await fs.mkdir(agentDir, { recursive: true }) await Filesystem.write( path.join(agentDir, "helper.md"), `--- model: test/model mode: subagent --- Helper subagent prompt`, ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["helper"]).toMatchObject({ name: "helper", model: "test/model", mode: "subagent", prompt: "Helper subagent prompt", }) }, }) }) test("merges instructions arrays from global and local configs", async () => { await using tmp = await tmpdir({ init: async (dir) => { const projectDir = path.join(dir, "project") const opencodeDir = path.join(projectDir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", instructions: ["global-instructions.md", "shared-rules.md"], }), ) await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", instructions: ["local-instructions.md"], }), ) }, }) await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { const config = await Config.get() const instructions = config.instructions ?? [] expect(instructions).toContain("global-instructions.md") expect(instructions).toContain("shared-rules.md") expect(instructions).toContain("local-instructions.md") expect(instructions.length).toBe(3) }, }) }) test("deduplicates duplicate instructions from global and local configs", async () => { await using tmp = await tmpdir({ init: async (dir) => { const projectDir = path.join(dir, "project") const opencodeDir = path.join(projectDir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", instructions: ["duplicate.md", "global-only.md"], }), ) await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", instructions: ["duplicate.md", "local-only.md"], }), ) }, }) await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { const config = await Config.get() const instructions = config.instructions ?? [] expect(instructions).toContain("global-only.md") expect(instructions).toContain("local-only.md") expect(instructions).toContain("duplicate.md") const duplicates = instructions.filter((i) => i === "duplicate.md") expect(duplicates.length).toBe(1) expect(instructions.length).toBe(3) }, }) }) test("deduplicates duplicate plugins from global and local configs", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Create a nested project structure with local .opencode config const projectDir = path.join(dir, "project") const opencodeDir = path.join(projectDir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) // Global config with plugins await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["duplicate-plugin", "global-plugin-1"], }), ) // Local .opencode config with some overlapping plugins await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["duplicate-plugin", "local-plugin-1"], }), ) }, }) await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { const config = await Config.get() const plugins = config.plugin ?? [] // Should contain all unique plugins expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) expect(plugins.some((p) => p.includes("duplicate-plugin"))).toBe(true) // Should deduplicate the duplicate plugin const duplicatePlugins = plugins.filter((p) => p.includes("duplicate-plugin")) expect(duplicatePlugins.length).toBe(1) // Should have exactly 3 unique plugins const pluginNames = plugins.filter( (p) => p.includes("global-plugin") || p.includes("local-plugin") || p.includes("duplicate-plugin"), ) expect(pluginNames.length).toBe(3) }, }) }) test("keeps plugin origins aligned with merged plugin list", async () => { await using tmp = await tmpdir({ init: async (dir) => { const project = path.join(dir, "project") const local = path.join(project, ".opencode") await fs.mkdir(local, { recursive: true }) await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: [["shared-plugin@1.0.0", { source: "global" }], "global-only@1.0.0"], }), ) await Filesystem.write( path.join(local, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"], }), ) }, }) await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { const cfg = await Config.get() const plugins = cfg.plugin ?? [] const origins = cfg.plugin_origins ?? [] const names = plugins.map((item) => Config.pluginSpecifier(item)) expect(names).toContain("shared-plugin@2.0.0") expect(names).not.toContain("shared-plugin@1.0.0") expect(names).toContain("global-only@1.0.0") expect(names).toContain("local-only@1.0.0") expect(origins.map((item) => item.spec)).toEqual(plugins) const hit = origins.find((item) => Config.pluginSpecifier(item.spec) === "shared-plugin@2.0.0") expect(hit?.scope).toBe("local") }, }) }) // Legacy tools migration tests test("migrates legacy tools config to permissions - allow", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { tools: { bash: true, read: true, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ bash: "allow", read: "allow", }) }, }) }) test("migrates legacy tools config to permissions - deny", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { tools: { bash: false, webfetch: false, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ bash: "deny", webfetch: "deny", }) }, }) }) test("migrates legacy write tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { tools: { write: true, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow", }) }, }) }) // Managed settings tests // Note: preload.ts sets OPENCODE_TEST_MANAGED_CONFIG which Global.Path.managedConfig uses test("managed settings override user settings", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", model: "user/model", share: "auto", username: "testuser", }) }, }) await writeManagedSettings({ $schema: "https://opencode.ai/config.json", model: "managed/model", share: "disabled", }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.model).toBe("managed/model") expect(config.share).toBe("disabled") expect(config.username).toBe("testuser") }, }) }) test("managed settings override project settings", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", autoupdate: true, disabled_providers: [], }) }, }) await writeManagedSettings({ $schema: "https://opencode.ai/config.json", autoupdate: false, disabled_providers: ["openai"], }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.autoupdate).toBe(false) expect(config.disabled_providers).toEqual(["openai"]) }, }) }) test("missing managed settings file is not an error", async () => { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", model: "user/model", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.model).toBe("user/model") }, }) }) test("migrates legacy edit tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { tools: { edit: false, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny", }) }, }) }) test("migrates legacy patch tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { tools: { patch: true, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow", }) }, }) }) test("migrates legacy multiedit tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { tools: { multiedit: false, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny", }) }, }) }) test("migrates mixed legacy tools config", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { tools: { bash: true, write: true, read: false, webfetch: true, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ bash: "allow", edit: "allow", read: "deny", webfetch: "allow", }) }, }) }) test("merges legacy tools with existing permission config", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", agent: { test: { permission: { glob: "allow", }, tools: { bash: true, }, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.agent?.["test"]?.permission).toEqual({ glob: "allow", bash: "allow", }) }, }) }) test("permission config preserves key order", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", permission: { "*": "deny", edit: "ask", write: "ask", external_directory: "ask", read: "allow", todowrite: "allow", "thoughts_*": "allow", "reasoning_model_*": "allow", "tools_*": "allow", "pr_comments_*": "allow", }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(Object.keys(config.permission!)).toEqual([ "*", "edit", "write", "external_directory", "read", "todowrite", "thoughts_*", "reasoning_model_*", "tools_*", "pr_comments_*", ]) }, }) }) // MCP config merging tests test("project config can override MCP server enabled status", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Simulates a base config (like from remote .well-known) with disabled MCP await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false, }, wiki: { type: "remote", url: "https://wiki.example.com/mcp", enabled: false, }, }, }), ) // Project config enables just jira await Filesystem.write( path.join(dir, "opencode.jsonc"), JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() // jira should be enabled (overridden by project config) expect(config.mcp?.jira).toEqual({ type: "remote", url: "https://jira.example.com/mcp", enabled: true, }) // wiki should still be disabled (not overridden) expect(config.mcp?.wiki).toEqual({ type: "remote", url: "https://wiki.example.com/mcp", enabled: false, }) }, }) }) test("MCP config deep merges preserving base config properties", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Base config with full MCP definition await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: { myserver: { type: "remote", url: "https://myserver.example.com/mcp", enabled: false, headers: { "X-Custom-Header": "value", }, }, }, }), ) // Override just enables it, should preserve other properties await Filesystem.write( path.join(dir, "opencode.jsonc"), JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: { myserver: { type: "remote", url: "https://myserver.example.com/mcp", enabled: true, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.mcp?.myserver).toEqual({ type: "remote", url: "https://myserver.example.com/mcp", enabled: true, headers: { "X-Custom-Header": "value", }, }) }, }) }) test("local .opencode config can override MCP from project config", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Project config with disabled MCP await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: { docs: { type: "remote", url: "https://docs.example.com/mcp", enabled: false, }, }, }), ) // Local .opencode directory config enables it const opencodeDir = path.join(dir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: { docs: { type: "remote", url: "https://docs.example.com/mcp", enabled: true, }, }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.mcp?.docs?.enabled).toBe(true) }, }) }) test("project config overrides remote well-known config", async () => { const originalFetch = globalThis.fetch let fetchedUrl: string | undefined globalThis.fetch = mock((url: string | URL | Request) => { const urlStr = url.toString() if (urlStr.includes(".well-known/opencode")) { fetchedUrl = urlStr return Promise.resolve( new Response( JSON.stringify({ config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } }, }, }), { status: 200 }, ), ) } return originalFetch(url) }) as unknown as typeof fetch const fakeAuth = Layer.mock(Auth.Service)({ all: () => Effect.succeed({ "https://example.com": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }), }), }) const layer = Config.layer.pipe( Layer.provide(AppFileSystem.defaultLayer), Layer.provide(fakeAuth), Layer.provide(emptyAccount), Layer.provideMerge(infra), ) try { await provideTmpdirInstance( () => Config.Service.use((svc) => Effect.gen(function* () { const config = yield* svc.get() expect(fetchedUrl).toBe("https://example.com/.well-known/opencode") expect(config.mcp?.jira?.enabled).toBe(true) }), ), { git: true, config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } }, }, ).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise) } finally { globalThis.fetch = originalFetch } }) test("wellknown URL with trailing slash is normalized", async () => { const originalFetch = globalThis.fetch let fetchedUrl: string | undefined globalThis.fetch = mock((url: string | URL | Request) => { const urlStr = url.toString() if (urlStr.includes(".well-known/opencode")) { fetchedUrl = urlStr return Promise.resolve( new Response( JSON.stringify({ config: { mcp: { slack: { type: "remote", url: "https://slack.example.com/mcp", enabled: true } }, }, }), { status: 200 }, ), ) } return originalFetch(url) }) as unknown as typeof fetch const fakeAuth = Layer.mock(Auth.Service)({ all: () => Effect.succeed({ "https://example.com/": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }), }), }) const layer = Config.layer.pipe( Layer.provide(AppFileSystem.defaultLayer), Layer.provide(fakeAuth), Layer.provide(emptyAccount), Layer.provideMerge(infra), ) try { await provideTmpdirInstance( () => Config.Service.use((svc) => Effect.gen(function* () { yield* svc.get() expect(fetchedUrl).toBe("https://example.com/.well-known/opencode") }), ), { git: true }, ).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise) } finally { globalThis.fetch = originalFetch } }) describe("resolvePluginSpec", () => { test("keeps package specs unchanged", async () => { await using tmp = await tmpdir() const file = path.join(tmp.path, "opencode.json") expect(await Config.resolvePluginSpec("oh-my-opencode@2.4.3", file)).toBe("oh-my-opencode@2.4.3") expect(await Config.resolvePluginSpec("@scope/pkg", file)).toBe("@scope/pkg") }) test("resolves windows-style relative plugin directory specs", async () => { if (process.platform !== "win32") return await using tmp = await tmpdir({ init: async (dir) => { const plugin = path.join(dir, "plugin") await fs.mkdir(plugin, { recursive: true }) await Filesystem.write(path.join(plugin, "index.ts"), "export default {}") }, }) const file = path.join(tmp.path, "opencode.json") const hit = await Config.resolvePluginSpec(".\\plugin", file) expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href) }) test("resolves relative file plugin paths to file urls", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write(path.join(dir, "plugin.ts"), "export default {}") }, }) const file = path.join(tmp.path, "opencode.json") const hit = await Config.resolvePluginSpec("./plugin.ts", file) expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin.ts")).href) }) test("resolves plugin directory paths to directory urls", async () => { await using tmp = await tmpdir({ init: async (dir) => { const plugin = path.join(dir, "plugin") await fs.mkdir(plugin, { recursive: true }) await Filesystem.writeJson(path.join(plugin, "package.json"), { name: "demo-plugin", type: "module", main: "./index.ts", }) await Filesystem.write(path.join(plugin, "index.ts"), "export default {}") }, }) const file = path.join(tmp.path, "opencode.json") const hit = await Config.resolvePluginSpec("./plugin", file) expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin")).href) }) test("resolves plugin directories without package.json to index.ts", async () => { await using tmp = await tmpdir({ init: async (dir) => { const plugin = path.join(dir, "plugin") await fs.mkdir(plugin, { recursive: true }) await Filesystem.write(path.join(plugin, "index.ts"), "export default {}") }, }) const file = path.join(tmp.path, "opencode.json") const hit = await Config.resolvePluginSpec("./plugin", file) expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href) }) }) describe("deduplicatePluginOrigins", () => { const dedupe = (plugins: Config.PluginSpec[]) => Config.deduplicatePluginOrigins( plugins.map((spec) => ({ spec, source: "", scope: "global" as const, })), ).map((item) => item.spec) test("removes duplicates keeping higher priority (later entries)", () => { const plugins = ["global-plugin@1.0.0", "shared-plugin@1.0.0", "local-plugin@2.0.0", "shared-plugin@2.0.0"] const result = dedupe(plugins) expect(result).toContain("global-plugin@1.0.0") expect(result).toContain("local-plugin@2.0.0") expect(result).toContain("shared-plugin@2.0.0") expect(result).not.toContain("shared-plugin@1.0.0") expect(result.length).toBe(3) }) test("keeps path plugins separate from package plugins", () => { const plugins = ["oh-my-opencode@2.4.3", "file:///project/.opencode/plugin/oh-my-opencode.js"] const result = dedupe(plugins) expect(result).toEqual(plugins) }) test("deduplicates direct path plugins by exact spec", () => { const plugins = ["file:///project/.opencode/plugin/demo.ts", "file:///project/.opencode/plugin/demo.ts"] const result = dedupe(plugins) expect(result).toEqual(["file:///project/.opencode/plugin/demo.ts"]) }) test("preserves order of remaining plugins", () => { const plugins = ["a-plugin@1.0.0", "b-plugin@1.0.0", "c-plugin@1.0.0"] const result = dedupe(plugins) expect(result).toEqual(["a-plugin@1.0.0", "b-plugin@1.0.0", "c-plugin@1.0.0"]) }) test("loads auto-discovered local plugins as file urls", async () => { await using tmp = await tmpdir({ init: async (dir) => { const projectDir = path.join(dir, "project") const opencodeDir = path.join(projectDir, ".opencode") const pluginDir = path.join(opencodeDir, "plugin") await fs.mkdir(pluginDir, { recursive: true }) await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["my-plugin@1.0.0"], }), ) await Filesystem.write(path.join(pluginDir, "my-plugin.js"), "export default {}") }, }) await Instance.provide({ directory: path.join(tmp.path, "project"), fn: async () => { const config = await Config.get() const plugins = config.plugin ?? [] expect(plugins.some((p) => Config.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true) expect(plugins.some((p) => Config.pluginSpecifier(p).startsWith("file://"))).toBe(true) }, }) }) }) describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { test("skips project config files when flag is set", async () => { const originalEnv = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" try { await using tmp = await tmpdir({ init: async (dir) => { // Create a project config that would normally be loaded await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", model: "project/model", username: "project-user", }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() // 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") }, }) } finally { if (originalEnv === undefined) { delete process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] } else { process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = originalEnv } } }) test("skips project .opencode/ directories when flag is set", async () => { const originalEnv = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" try { await using tmp = await tmpdir({ init: async (dir) => { // Create a .opencode directory with a command const opencodeDir = path.join(dir, ".opencode", "command") await fs.mkdir(opencodeDir, { recursive: true }) await Filesystem.write(path.join(opencodeDir, "test-cmd.md"), "# Test Command\nThis is a test command.") }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const directories = await Config.directories() // Project .opencode should NOT be in directories list const hasProjectOpencode = directories.some((d) => d.startsWith(tmp.path)) expect(hasProjectOpencode).toBe(false) }, }) } finally { if (originalEnv === undefined) { delete process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] } else { process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = originalEnv } } }) test("still loads global config when flag is set", async () => { const originalEnv = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" try { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { // Should still get default config (from global or defaults) const config = await Config.get() expect(config).toBeDefined() expect(config.username).toBeDefined() }, }) } finally { if (originalEnv === undefined) { delete process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] } else { process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = originalEnv } } }) test("skips relative instructions with warning when flag is set but no config dir", async () => { const originalDisable = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] const originalConfigDir = process.env["OPENCODE_CONFIG_DIR"] try { // Ensure no config dir is set delete process.env["OPENCODE_CONFIG_DIR"] process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" await using tmp = await tmpdir({ init: async (dir) => { // Create a config with relative instruction path await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", instructions: ["./CUSTOM.md"], }), ) // Create the instruction file (should be skipped) await Filesystem.write(path.join(dir, "CUSTOM.md"), "# Custom Instructions") }, }) await Instance.provide({ directory: tmp.path, 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() expect(config).toBeDefined() // The instruction should have been skipped (warning logged) // We can't easily test the warning was logged, but we verify // the relative path didn't cause an error }, }) } finally { if (originalDisable === undefined) { delete process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] } else { process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = originalDisable } if (originalConfigDir === undefined) { delete process.env["OPENCODE_CONFIG_DIR"] } else { process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir } } }) test("OPENCODE_CONFIG_DIR still works when flag is set", async () => { const originalDisable = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] const originalConfigDir = process.env["OPENCODE_CONFIG_DIR"] try { await using configDirTmp = await tmpdir({ init: async (dir) => { // Create config in the custom config dir await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", model: "configdir/model", }), ) }, }) await using projectTmp = await tmpdir({ init: async (dir) => { // Create config in project (should be ignored) await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", model: "project/model", }), ) }, }) process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" process.env["OPENCODE_CONFIG_DIR"] = configDirTmp.path await Instance.provide({ directory: projectTmp.path, fn: async () => { const config = await Config.get() // Should load from OPENCODE_CONFIG_DIR, not project expect(config.model).toBe("configdir/model") }, }) } finally { if (originalDisable === undefined) { delete process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] } else { process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = originalDisable } if (originalConfigDir === undefined) { delete process.env["OPENCODE_CONFIG_DIR"] } else { process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir } } }) }) describe("OPENCODE_CONFIG_CONTENT token substitution", () => { test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => { const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] const originalTestVar = process.env["TEST_CONFIG_VAR"] process.env["TEST_CONFIG_VAR"] = "test_api_key_12345" process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ $schema: "https://opencode.ai/config.json", username: "{env:TEST_CONFIG_VAR}", }) try { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.username).toBe("test_api_key_12345") }, }) } finally { if (originalEnv !== undefined) { process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv } else { delete process.env["OPENCODE_CONFIG_CONTENT"] } if (originalTestVar !== undefined) { process.env["TEST_CONFIG_VAR"] = originalTestVar } else { delete process.env["TEST_CONFIG_VAR"] } } }) test("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", async () => { const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] try { await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write(path.join(dir, "api_key.txt"), "secret_key_from_file") process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ $schema: "https://opencode.ai/config.json", username: "{file:./api_key.txt}", }) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const config = await Config.get() expect(config.username).toBe("secret_key_from_file") }, }) } finally { if (originalEnv !== undefined) { process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv } else { delete process.env["OPENCODE_CONFIG_CONTENT"] } } }) }) // parseManagedPlist unit tests — pure function, no OS interaction test("parseManagedPlist strips MDM metadata keys", async () => { const config = await Config.parseManagedPlist( JSON.stringify({ PayloadDisplayName: "OpenCode Managed", PayloadIdentifier: "ai.opencode.managed.test", PayloadType: "ai.opencode.managed", PayloadUUID: "AAAA-BBBB-CCCC", PayloadVersion: 1, _manualProfile: true, share: "disabled", model: "mdm/model", }), "test:mobileconfig", ) expect(config.share).toBe("disabled") expect(config.model).toBe("mdm/model") // MDM keys must not leak into the parsed config expect((config as any).PayloadUUID).toBeUndefined() expect((config as any).PayloadType).toBeUndefined() expect((config as any)._manualProfile).toBeUndefined() }) test("parseManagedPlist parses server settings", async () => { const config = await Config.parseManagedPlist( JSON.stringify({ $schema: "https://opencode.ai/config.json", server: { hostname: "127.0.0.1", mdns: false }, autoupdate: true, }), "test:mobileconfig", ) expect(config.server?.hostname).toBe("127.0.0.1") expect(config.server?.mdns).toBe(false) expect(config.autoupdate).toBe(true) }) test("parseManagedPlist parses permission rules", async () => { const config = await Config.parseManagedPlist( JSON.stringify({ $schema: "https://opencode.ai/config.json", permission: { "*": "ask", bash: { "*": "ask", "rm -rf *": "deny", "curl *": "deny" }, grep: "allow", glob: "allow", webfetch: "ask", "~/.ssh/*": "deny", }, }), "test:mobileconfig", ) expect(config.permission?.["*"]).toBe("ask") expect(config.permission?.grep).toBe("allow") expect(config.permission?.webfetch).toBe("ask") expect(config.permission?.["~/.ssh/*"]).toBe("deny") const bash = config.permission?.bash as Record expect(bash?.["rm -rf *"]).toBe("deny") expect(bash?.["curl *"]).toBe("deny") }) test("parseManagedPlist parses enabled_providers", async () => { const config = await Config.parseManagedPlist( JSON.stringify({ $schema: "https://opencode.ai/config.json", enabled_providers: ["anthropic", "google"], }), "test:mobileconfig", ) expect(config.enabled_providers).toEqual(["anthropic", "google"]) }) test("parseManagedPlist handles empty config", async () => { const config = await Config.parseManagedPlist( JSON.stringify({ $schema: "https://opencode.ai/config.json" }), "test:mobileconfig", ) expect(config.$schema).toBe("https://opencode.ai/config.json") })