diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index 199d075620..3abd62ba6d 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -1,5 +1,5 @@
import { test, expect, describe, mock, afterEach, beforeEach } from "bun:test"
-import { Effect, Layer, Option } from "effect"
+import { Effect, Exit, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed"
@@ -13,7 +13,7 @@ import { Account } from "../../src/account/account"
import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Env } from "../../src/env"
-import { provideTestInstance, provideTmpdirInstance, withTestInstance } from "../fixture/fixture"
+import { provideTestInstance, provideTmpdirInstance, TestInstance, withTestInstance } from "../fixture/fixture"
import { tmpdir } from "../fixture/fixture"
import { InstanceRuntime } from "@/project/instance-runtime"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -115,6 +115,25 @@ async function writeConfig(dir: string, config: object, name = "opencode.json")
await Filesystem.write(path.join(dir, name), JSON.stringify(config))
}
+const writeConfigEffect = (dir: string, config: object, name = "opencode.json") =>
+ Effect.promise(() => writeConfig(dir, config, name))
+
+function withProcessEnv(key: string, value: string, effect: Effect.Effect) {
+ return Effect.acquireUseRelease(
+ Effect.sync(() => {
+ const original = process.env[key]
+ process.env[key] = value
+ return original
+ }),
+ () => effect,
+ (original) =>
+ Effect.sync(() => {
+ if (original !== undefined) process.env[key] = original
+ else delete process.env[key]
+ }),
+ )
+}
+
async function check(map: (dir: string) => string) {
if (process.platform !== "win32") return
await using globalTmp = await tmpdir()
@@ -360,124 +379,118 @@ test("ignores legacy tui keys in opencode config", async () => {
})
})
-test("loads JSONC config file", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Filesystem.write(
- path.join(dir, "opencode.jsonc"),
+it.instance("loads JSONC config file", () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* Effect.promise(() =>
+ Filesystem.write(
+ path.join(test.directory, "opencode.jsonc"),
`{
// This is a comment
"$schema": "https://opencode.ai/config.json",
"model": "test/model",
"username": "testuser"
}`,
- )
- },
- })
- await withTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- const config = await load(ctx)
- expect(config.model).toBe("test/model")
- expect(config.username).toBe("testuser")
- },
- })
-})
+ ),
+ )
+ const config = yield* Config.Service.use((svc) => svc.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, {
+it.instance("jsonc overrides json in the same directory", () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* writeConfigEffect(
+ test.directory,
+ {
$schema: "https://opencode.ai/config.json",
- model: "override",
+ model: "base",
+ username: "base",
+ },
+ "opencode.jsonc",
+ )
+ yield* writeConfigEffect(test.directory, {
+ $schema: "https://opencode.ai/config.json",
+ model: "override",
+ })
+ const config = yield* Config.Service.use((svc) => svc.get())
+ expect(config.model).toBe("base")
+ expect(config.username).toBe("base")
+ }),
+)
+
+it.instance("handles environment variable substitution", () =>
+ withProcessEnv(
+ "TEST_VAR",
+ "test-user",
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* writeConfigEffect(test.directory, {
+ $schema: "https://opencode.ai/config.json",
+ username: "{env:TEST_VAR}",
})
- },
- })
- await withTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- const config = await load(ctx)
- expect(config.model).toBe("base")
- expect(config.username).toBe("base")
- },
- })
-})
+ const config = yield* Config.Service.use((svc) => svc.get())
+ expect(config.username).toBe("test-user")
+ }),
+ ),
+)
-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 withTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- const config = await load(ctx)
- 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"),
+it.instance("preserves env variables when adding $schema to config", () =>
+ withProcessEnv(
+ "PRESERVE_VAR",
+ "secret_value",
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ // Config without $schema - should trigger auto-add
+ yield* Effect.promise(() =>
+ Filesystem.write(
+ path.join(test.directory, "opencode.json"),
JSON.stringify({
username: "{env:PRESERVE_VAR}",
}),
- )
- },
- })
- await withTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- const config = await load(ctx)
- expect(config.username).toBe("secret_value")
+ ),
+ )
+ const config = yield* Config.Service.use((svc) => svc.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")
- },
+ // Read the file to verify the env variable was preserved
+ const content = yield* Effect.promise(() => Filesystem.readText(path.join(test.directory, "opencode.json")))
+ expect(content).toContain("{env:PRESERVE_VAR}")
+ expect(content).not.toContain("secret_value")
+ expect(content).toContain("$schema")
+ }),
+ ),
+)
+
+it.instance("handles file inclusion substitution", () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* Effect.promise(() => Filesystem.write(path.join(test.directory, "included.txt"), "test-user"))
+ yield* writeConfigEffect(test.directory, {
+ $schema: "https://opencode.ai/config.json",
+ username: "{file:included.txt}",
})
- } finally {
- if (originalEnv !== undefined) {
- process.env["PRESERVE_VAR"] = originalEnv
- } else {
- delete process.env["PRESERVE_VAR"]
- }
- }
-})
+ const config = yield* Config.Service.use((svc) => svc.get())
+ expect(config.username).toBe("test-user")
+ }),
+)
+
+it.instance("handles file inclusion with replacement tokens", () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* Effect.promise(() =>
+ Filesystem.write(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`"),
+ )
+ yield* writeConfigEffect(test.directory, {
+ $schema: "https://opencode.ai/config.json",
+ username: "{file:included.md}",
+ })
+ const config = yield* Config.Service.use((svc) => svc.get())
+ expect(config.username).toBe("const out = await Bun.$`echo hi`")
+ }),
+)
test("resolves env templates in account config with account token", async () => {
const originalControlToken = process.env["OPENCODE_CONSOLE_TOKEN"]
@@ -544,105 +557,50 @@ test("resolves env templates in account config with account token", async () =>
}
})
-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 withTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- const config = await load(ctx)
- expect(config.username).toBe("test-user")
- },
- })
-})
+it.instance("validates config schema and throws on invalid fields", () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* writeConfigEffect(test.directory, {
+ $schema: "https://opencode.ai/config.json",
+ invalid_field: "should cause error",
+ })
+ const exit = yield* Config.Service.use((svc) => svc.get()).pipe(Effect.exit)
+ expect(Exit.isFailure(exit)).toBe(true)
+ }),
+)
-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 withTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- const config = await load(ctx)
- expect(config.username).toBe("const out = await Bun.$`echo hi`")
- },
- })
-})
+it.instance("throws error for invalid JSON", () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* Effect.promise(() => Filesystem.write(path.join(test.directory, "opencode.json"), "{ invalid json }"))
+ const exit = yield* Config.Service.use((svc) => svc.get()).pipe(Effect.exit)
+ expect(Exit.isFailure(exit)).toBe(true)
+ }),
+)
-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 provideTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- // Strict schema should throw an error for invalid fields
- await expect(load(ctx)).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 provideTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- await expect(load(ctx)).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 withTestInstance({
- directory: tmp.path,
- fn: async (ctx) => {
- const config = await load(ctx)
- expect(config.agent?.["test_agent"]).toEqual(
- expect.objectContaining({
+it.instance("handles agent configuration", () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ yield* writeConfigEffect(test.directory, {
+ $schema: "https://opencode.ai/config.json",
+ agent: {
+ test_agent: {
model: "test/model",
temperature: 0.7,
description: "test agent",
- }),
- )
- },
- })
-})
+ },
+ },
+ })
+ const config = yield* Config.Service.use((svc) => svc.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({
diff --git a/perf/test-suite.md b/perf/test-suite.md
index a377e513c8..e7420cd519 100644
--- a/perf/test-suite.md
+++ b/perf/test-suite.md
@@ -72,6 +72,7 @@ Repeated setup work, long sleeps/timeouts, serial integration tests, filesystem/
| Custom provider/model config cases can use Effect-aware instance fixtures | Migrated three more config-heavy provider cases to `it.instance` | 6.07s | 6.12s | keep | Neutral timing within noise, but continues removing manual config file writes on top of the first provider fixture PR. |
| Provider env precedence and model lookup cases can use Effect-aware instance fixtures | Migrated four more provider lookup/default-model cases to `it.instance` | 6.12s | 6.36s | keep | Noisy 5-run median; kept as a small stacked cleanup slice but do not claim speedup from this migration. |
| Simple config load cases can use Effect-aware instance fixtures | Migrated JSON, shell, formatter, and lsp config load cases to `it.instance` | 14.18s | 3.93s | keep | Three-run medians before/after; removes manual `tmpdir` + `withTestInstance` setup from the first simple config block. |
+| Config template, file include, and simple agent cases can use Effect-aware instance fixtures | Migrated JSONC, env/file substitution, invalid config, and agent config cases to `it.instance` | 1.87s | 1.90s | keep | Stacked on the first config slice; neutral timing but removes more manual `tmpdir` + instance plumbing. |
## Profiling Results