From 338666d13eeecccc817989ede2c0415b19d1fd97 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 18 May 2026 19:37:58 -0400 Subject: [PATCH] Migrate config template tests to instance fixtures (#28211) --- packages/opencode/test/config/config.test.ts | 356 ++++++++----------- perf/test-suite.md | 1 + 2 files changed, 158 insertions(+), 199 deletions(-) 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