Compare commits

...

3 Commits

Author SHA1 Message Date
Kit Langton 16d8545e72 test: migrate config agent fixtures 2026-05-18 14:31:46 -04:00
Kit Langton 4828443997 test: migrate config template fixtures 2026-05-18 14:31:43 -04:00
Kit Langton c8c01f5275 test: migrate simple config fixtures 2026-05-18 14:31:37 -04:00
2 changed files with 273 additions and 388 deletions
+172 -290
View File
@@ -1,5 +1,5 @@
import { test, expect, describe, mock, afterEach, beforeEach } from "bun:test" 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 { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed" import { ConfigManaged } from "@/config/managed"
@@ -13,7 +13,7 @@ import { Account } from "../../src/account/account"
import { AccessToken, AccountID, OrgID } from "../../src/account/schema" import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Env } from "../../src/env" 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 { tmpdir } from "../fixture/fixture"
import { InstanceRuntime } from "@/project/instance-runtime" import { InstanceRuntime } from "@/project/instance-runtime"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -94,14 +94,6 @@ const listDirs = (ctx: InstanceContext) =>
Effect.provide(layer), Effect.provide(layer),
), ),
) )
const ready = (ctx: InstanceContext) =>
Effect.runPromise(
Config.Service.use((svc) => provideCurrentInstance(svc.waitForDependencies(), ctx)).pipe(
Effect.scoped,
Effect.provide(layer),
),
)
// Get managed config directory from environment (set in preload.ts) // Get managed config directory from environment (set in preload.ts)
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
@@ -123,6 +115,25 @@ async function writeConfig(dir: string, config: object, name = "opencode.json")
await Filesystem.write(path.join(dir, name), JSON.stringify(config)) 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<A, E, R>(key: string, value: string, effect: Effect.Effect<A, E, R>) {
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) { async function check(map: (dir: string) => string) {
if (process.platform !== "win32") return if (process.platform !== "win32") return
await using globalTmp = await tmpdir() await using globalTmp = await tmpdir()
@@ -210,43 +221,24 @@ test("does not create global config when OPENCODE_CONFIG_DIR is set", async () =
} }
}) })
test("loads JSON config file", async () => { it.instance(
await using tmp = await tmpdir({ "loads JSON config file",
init: async (dir) => { Effect.gen(function* () {
await writeConfig(dir, { const config = yield* Config.Service.use((svc) => svc.get())
$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.model).toBe("test/model")
expect(config.username).toBe("testuser") expect(config.username).toBe("testuser")
}, }),
}) { config: { model: "test/model", username: "testuser" } },
}) )
test("loads shell config field", async () => { it.instance(
await using tmp = await tmpdir({ "loads shell config field",
init: async (dir) => { Effect.gen(function* () {
await writeConfig(dir, { const config = yield* Config.Service.use((svc) => svc.get())
$schema: "https://opencode.ai/config.json",
shell: "bash",
})
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.shell).toBe("bash") expect(config.shell).toBe("bash")
}, }),
}) { config: { shell: "bash" } },
}) )
test("updates config and preserves empty shell sentinel", async () => { test("updates config and preserves empty shell sentinel", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
@@ -330,41 +322,23 @@ test("updates global config and omits empty shell key in jsonc", async () => {
} }
}) })
test("loads formatter boolean config", async () => { it.instance(
await using tmp = await tmpdir({ "loads formatter boolean config",
init: async (dir) => { Effect.gen(function* () {
await writeConfig(dir, { const config = yield* Config.Service.use((svc) => svc.get())
$schema: "https://opencode.ai/config.json",
formatter: true,
})
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.formatter).toBe(true) expect(config.formatter).toBe(true)
}, }),
}) { config: { formatter: true } },
}) )
test("loads lsp boolean config", async () => { it.instance(
await using tmp = await tmpdir({ "loads lsp boolean config",
init: async (dir) => { Effect.gen(function* () {
await writeConfig(dir, { const config = yield* Config.Service.use((svc) => svc.get())
$schema: "https://opencode.ai/config.json",
lsp: true,
})
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.lsp).toBe(true) expect(config.lsp).toBe(true)
}, }),
}) { config: { lsp: true } },
}) )
test("loads project config from Git Bash and MSYS2 paths on Windows", async () => { test("loads project config from Git Bash and MSYS2 paths on Windows", async () => {
// Git Bash and MSYS2 both use /<drive>/... paths on Windows. // Git Bash and MSYS2 both use /<drive>/... paths on Windows.
@@ -405,35 +379,31 @@ test("ignores legacy tui keys in opencode config", async () => {
}) })
}) })
test("loads JSONC config file", async () => { it.instance("loads JSONC config file", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await Filesystem.write( yield* Effect.promise(() =>
path.join(dir, "opencode.jsonc"), Filesystem.write(
path.join(test.directory, "opencode.jsonc"),
`{ `{
// This is a comment // This is a comment
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"model": "test/model", "model": "test/model",
"username": "testuser" "username": "testuser"
}`, }`,
),
) )
}, const config = yield* Config.Service.use((svc) => svc.get())
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.model).toBe("test/model") expect(config.model).toBe("test/model")
expect(config.username).toBe("testuser") expect(config.username).toBe("testuser")
}, }),
}) )
})
test("jsonc overrides json in the same directory", async () => { it.instance("jsonc overrides json in the same directory", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await writeConfig( yield* writeConfigEffect(
dir, test.directory,
{ {
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
model: "base", model: "base",
@@ -441,88 +411,86 @@ test("jsonc overrides json in the same directory", async () => {
}, },
"opencode.jsonc", "opencode.jsonc",
) )
await writeConfig(dir, { yield* writeConfigEffect(test.directory, {
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
model: "override", model: "override",
}) })
}, const config = yield* Config.Service.use((svc) => svc.get())
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.model).toBe("base") expect(config.model).toBe("base")
expect(config.username).toBe("base") expect(config.username).toBe("base")
}, }),
}) )
})
test("handles environment variable substitution", async () => { it.instance("handles environment variable substitution", () =>
const originalEnv = process.env["TEST_VAR"] withProcessEnv(
process.env["TEST_VAR"] = "test-user" "TEST_VAR",
"test-user",
try { Effect.gen(function* () {
await using tmp = await tmpdir({ const test = yield* TestInstance
init: async (dir) => { yield* writeConfigEffect(test.directory, {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
username: "{env:TEST_VAR}", username: "{env:TEST_VAR}",
}) })
}, const config = yield* Config.Service.use((svc) => svc.get())
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.username).toBe("test-user") 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 () => { it.instance("preserves env variables when adding $schema to config", () =>
const originalEnv = process.env["PRESERVE_VAR"] withProcessEnv(
process.env["PRESERVE_VAR"] = "secret_value" "PRESERVE_VAR",
"secret_value",
try { Effect.gen(function* () {
await using tmp = await tmpdir({ const test = yield* TestInstance
init: async (dir) => {
// Config without $schema - should trigger auto-add // Config without $schema - should trigger auto-add
await Filesystem.write( yield* Effect.promise(() =>
path.join(dir, "opencode.json"), Filesystem.write(
path.join(test.directory, "opencode.json"),
JSON.stringify({ JSON.stringify({
username: "{env:PRESERVE_VAR}", username: "{env:PRESERVE_VAR}",
}), }),
),
) )
}, const config = yield* Config.Service.use((svc) => svc.get())
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.username).toBe("secret_value") expect(config.username).toBe("secret_value")
// Read the file to verify the env variable was preserved // Read the file to verify the env variable was preserved
const content = await Filesystem.readText(path.join(tmp.path, "opencode.json")) const content = yield* Effect.promise(() => Filesystem.readText(path.join(test.directory, "opencode.json")))
expect(content).toContain("{env:PRESERVE_VAR}") expect(content).toContain("{env:PRESERVE_VAR}")
expect(content).not.toContain("secret_value") expect(content).not.toContain("secret_value")
expect(content).toContain("$schema") 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 { const config = yield* Config.Service.use((svc) => svc.get())
if (originalEnv !== undefined) { expect(config.username).toBe("test-user")
process.env["PRESERVE_VAR"] = originalEnv }),
} else { )
delete process.env["PRESERVE_VAR"]
} 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 () => { test("resolves env templates in account config with account token", async () => {
const originalControlToken = process.env["OPENCODE_CONSOLE_TOKEN"] const originalControlToken = process.env["OPENCODE_CONSOLE_TOKEN"]
@@ -589,80 +557,31 @@ test("resolves env templates in account config with account token", async () =>
} }
}) })
test("handles file inclusion substitution", async () => { it.instance("validates config schema and throws on invalid fields", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await Filesystem.write(path.join(dir, "included.txt"), "test-user") yield* writeConfigEffect(test.directory, {
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")
},
})
})
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`")
},
})
})
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", $schema: "https://opencode.ai/config.json",
invalid_field: "should cause error", invalid_field: "should cause error",
}) })
}, const exit = yield* Config.Service.use((svc) => svc.get()).pipe(Effect.exit)
}) expect(Exit.isFailure(exit)).toBe(true)
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 () => { it.instance("throws error for invalid JSON", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await Filesystem.write(path.join(dir, "opencode.json"), "{ invalid json }") 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)
await provideTestInstance({ }),
directory: tmp.path, )
fn: async (ctx) => {
await expect(load(ctx)).rejects.toThrow()
},
})
})
test("handles agent configuration", async () => { it.instance("handles agent configuration", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await writeConfig(dir, { yield* writeConfigEffect(test.directory, {
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
agent: { agent: {
test_agent: { test_agent: {
@@ -672,12 +591,7 @@ test("handles agent configuration", async () => {
}, },
}, },
}) })
}, const config = yield* Config.Service.use((svc) => svc.get())
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.agent?.["test_agent"]).toEqual( expect(config.agent?.["test_agent"]).toEqual(
expect.objectContaining({ expect.objectContaining({
model: "test/model", model: "test/model",
@@ -685,14 +599,13 @@ test("handles agent configuration", async () => {
description: "test agent", description: "test agent",
}), }),
) )
}, }),
}) )
})
test("treats agent variant as model-scoped setting (not provider option)", async () => { it.instance("treats agent variant as model-scoped setting (not provider option)", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await writeConfig(dir, { yield* writeConfigEffect(test.directory, {
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
agent: { agent: {
test_agent: { test_agent: {
@@ -702,13 +615,7 @@ test("treats agent variant as model-scoped setting (not provider option)", async
}, },
}, },
}) })
}, const config = yield* Config.Service.use((svc) => svc.get())
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
const agent = config.agent?.["test_agent"] const agent = config.agent?.["test_agent"]
expect(agent?.variant).toBe("xhigh") expect(agent?.variant).toBe("xhigh")
@@ -716,14 +623,13 @@ test("treats agent variant as model-scoped setting (not provider option)", async
max_tokens: 123, max_tokens: 123,
}) })
expect(agent?.options).not.toHaveProperty("variant") expect(agent?.options).not.toHaveProperty("variant")
}, }),
}) )
})
test("handles command configuration", async () => { it.instance("handles command configuration", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await writeConfig(dir, { yield* writeConfigEffect(test.directory, {
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
command: { command: {
test_command: { test_command: {
@@ -733,49 +639,32 @@ test("handles command configuration", async () => {
}, },
}, },
}) })
}, const config = yield* Config.Service.use((svc) => svc.get())
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.command?.["test_command"]).toEqual({ expect(config.command?.["test_command"]).toEqual({
template: "test template", template: "test template",
description: "test command", description: "test command",
agent: "test_agent", agent: "test_agent",
}) })
}, }),
}) )
})
test("migrates autoshare to share field", async () => { it.instance("migrates autoshare to share field", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await Filesystem.write( yield* writeConfigEffect(test.directory, {
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
autoshare: true, autoshare: true,
}),
)
},
}) })
await withTestInstance({ const config = yield* Config.Service.use((svc) => svc.get())
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.share).toBe("auto") expect(config.share).toBe("auto")
expect(config.autoshare).toBe(true) expect(config.autoshare).toBe(true)
}, }),
}) )
})
test("migrates mode field to agent field", async () => { it.instance("migrates mode field to agent field", () =>
await using tmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const test = yield* TestInstance
await Filesystem.write( yield* writeConfigEffect(test.directory, {
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
mode: { mode: {
test_mode: { test_mode: {
@@ -783,14 +672,8 @@ test("migrates mode field to agent field", async () => {
temperature: 0.5, temperature: 0.5,
}, },
}, },
}),
)
},
}) })
await withTestInstance({ const config = yield* Config.Service.use((svc) => svc.get())
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.agent?.["test_mode"]).toEqual({ expect(config.agent?.["test_mode"]).toEqual({
model: "test/model", model: "test/model",
temperature: 0.5, temperature: 0.5,
@@ -798,9 +681,8 @@ test("migrates mode field to agent field", async () => {
options: {}, options: {},
permission: {}, permission: {},
}) })
}, }),
}) )
})
test("loads config from .opencode directory", async () => { test("loads config from .opencode directory", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
+3
View File
@@ -71,6 +71,9 @@ Repeated setup work, long sleeps/timeouts, serial integration tests, filesystem/
| First provider config/env/filtering block can use Effect-aware instance fixtures | Migrated six `tmpdir` + `withTestInstance` cases to `it.instance` | 6.06s | 6.07s | keep | Neutral timing, but removes manual config file writes and instance plumbing; use as the pattern for later provider slices. | | First provider config/env/filtering block can use Effect-aware instance fixtures | Migrated six `tmpdir` + `withTestInstance` cases to `it.instance` | 6.06s | 6.07s | keep | Neutral timing, but removes manual config file writes and instance plumbing; use as the pattern for later provider slices. |
| 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. | | 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. | | 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. |
| Agent option, command, and legacy migration config cases can use Effect-aware instance fixtures | Migrated agent variant, command, autoshare, and mode migration cases to `it.instance` | 1.90s | 1.83s | keep | Stacked on the config template slice; small neutral-to-positive timing and less manual setup. |
## Profiling Results ## Profiling Results