test(config): port env-var config tests to it.instance (#28706)

This commit is contained in:
Kit Langton
2026-05-21 19:17:29 +00:00
committed by GitHub
parent d70942079a
commit 231689c767
+135 -275
View File
@@ -13,8 +13,14 @@ 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, TestInstance, withTestInstance } from "../fixture/fixture" import {
import { tmpdir } from "../fixture/fixture" provideTestInstance,
provideTmpdirInstance,
TestInstance,
tmpdir,
tmpdirScoped,
withTestInstance,
} 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"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
@@ -116,18 +122,31 @@ const writeConfigEffect = (dir: string, config: object, name = "opencode.json")
const mkdirEffect = (dir: string) => Effect.promise(() => fs.mkdir(dir, { recursive: true })) const mkdirEffect = (dir: string) => Effect.promise(() => fs.mkdir(dir, { recursive: true }))
const writeTextEffect = (file: string, content: string) => Effect.promise(() => Filesystem.write(file, content)) const writeTextEffect = (file: string, content: string) => Effect.promise(() => Filesystem.write(file, content))
function withProcessEnv<A, E, R>(key: string, value: string, effect: Effect.Effect<A, E, R>) { function withProcessEnv<A, E, R>(key: string, value: string | undefined, effect: Effect.Effect<A, E, R>) {
return withProcessEnvs({ [key]: value }, effect)
}
function withProcessEnvs<A, E, R>(
entries: Record<string, string | undefined>,
effect: Effect.Effect<A, E, R>,
) {
return Effect.acquireUseRelease( return Effect.acquireUseRelease(
Effect.sync(() => { Effect.sync(() => {
const original = process.env[key] const originals: Record<string, string | undefined> = {}
process.env[key] = value for (const [key, value] of Object.entries(entries)) {
return original originals[key] = process.env[key]
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
return originals
}), }),
() => effect, () => effect,
(original) => (originals) =>
Effect.sync(() => { Effect.sync(() => {
if (original !== undefined) process.env[key] = original for (const [key, original] of Object.entries(originals)) {
else delete process.env[key] if (original !== undefined) process.env[key] = original
else delete process.env[key]
}
}), }),
) )
} }
@@ -1881,293 +1900,134 @@ describe("deduplicatePluginOrigins", () => {
}) })
describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
test("skips project config files when flag is set", async () => { it.instance(
const originalEnv = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] "skips project config files when flag is set",
process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" () =>
withProcessEnv(
try { "OPENCODE_DISABLE_PROJECT_CONFIG",
await using tmp = await tmpdir({ "true",
init: async (dir) => { Effect.gen(function* () {
// Create a project config that would normally be loaded const config = yield* Config.use.get()
await Filesystem.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
model: "project/model",
username: "project-user",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
// Project config should NOT be loaded - model should be default, not "project/model"
expect(config.model).not.toBe("project/model") expect(config.model).not.toBe("project/model")
expect(config.username).not.toBe("project-user") expect(config.username).not.toBe("project-user")
}, }),
}) ),
} finally { { config: { model: "project/model", username: "project-user" } },
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 () => { it.instance("skips project .opencode/ directories when flag is set", () =>
const originalEnv = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] withProcessEnv(
process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" "OPENCODE_DISABLE_PROJECT_CONFIG",
"true",
Effect.gen(function* () {
const test = yield* TestInstance
yield* mkdirEffect(path.join(test.directory, ".opencode", "command"))
yield* writeTextEffect(
path.join(test.directory, ".opencode", "command", "test-cmd.md"),
"# Test Command\nThis is a test command.",
)
const directories = yield* Config.use.directories()
expect(directories.some((d) => d.startsWith(test.directory))).toBe(false)
}),
),
)
try { it.instance("still loads global config when flag is set", () =>
await using tmp = await tmpdir({ withProcessEnv(
init: async (dir) => { "OPENCODE_DISABLE_PROJECT_CONFIG",
// Create a .opencode directory with a command "true",
const opencodeDir = path.join(dir, ".opencode", "command") Effect.gen(function* () {
await fs.mkdir(opencodeDir, { recursive: true }) const config = yield* Config.use.get()
await Filesystem.write(path.join(opencodeDir, "test-cmd.md"), "# Test Command\nThis is a test command.") expect(config).toBeDefined()
}, expect(config.username).toBeDefined()
}) }),
await withTestInstance({ ),
directory: tmp.path, )
fn: async (ctx) => {
const directories = await listDirs(ctx)
// 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 () => { it.instance(
const originalEnv = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] "skips relative instructions with warning when flag is set but no config dir",
process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true" () =>
withProcessEnvs(
try { { OPENCODE_CONFIG_DIR: undefined, OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
await using tmp = await tmpdir() Effect.gen(function* () {
await withTestInstance({ const test = yield* TestInstance
directory: tmp.path, yield* writeTextEffect(path.join(test.directory, "CUSTOM.md"), "# Custom Instructions")
fn: async (ctx) => {
// Should still get default config (from global or defaults)
const config = await load(ctx)
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 withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
// The relative instruction should be skipped without error // The relative instruction should be skipped without error
// We're mainly verifying this doesn't throw and the config loads const config = yield* Config.use.get()
const config = await load(ctx)
expect(config).toBeDefined() 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 { config: { instructions: ["./CUSTOM.md"] } },
}, )
})
} 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 () => { it.instance("OPENCODE_CONFIG_DIR still works when flag is set", () =>
const originalDisable = process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] Effect.gen(function* () {
const originalConfigDir = process.env["OPENCODE_CONFIG_DIR"] const configDir = yield* tmpdirScoped({ config: { model: "configdir/model" } })
yield* withProcessEnvs(
try { { OPENCODE_DISABLE_PROJECT_CONFIG: "true", OPENCODE_CONFIG_DIR: configDir },
await using configDirTmp = await tmpdir({ Effect.gen(function* () {
init: async (dir) => { const config = yield* Config.use.get()
// 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 withTestInstance({
directory: projectTmp.path,
fn: async (ctx) => {
const config = await load(ctx)
// Should load from OPENCODE_CONFIG_DIR, not project
expect(config.model).toBe("configdir/model") expect(config.model).toBe("configdir/model")
}, }),
}) )
} finally { }),
if (originalDisable === undefined) { { config: { model: "project/model" } },
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
}
}
})
}) })
// Regression for #28206: malformed OPENCODE_PERMISSION JSON used to crash // Regression for #28206: malformed OPENCODE_PERMISSION JSON used to crash
// the app on startup with an unhandled SyntaxError. Loading the config with // the app on startup with an unhandled SyntaxError. Loading the config with
// an invalid JSON value in this env var should not throw. // an invalid JSON value in this env var should not throw.
describe("OPENCODE_PERMISSION env var", () => { describe("OPENCODE_PERMISSION env var", () => {
test("does not crash when OPENCODE_PERMISSION contains invalid JSON", async () => { it.instance("does not crash when OPENCODE_PERMISSION contains invalid JSON", () =>
const original = process.env["OPENCODE_PERMISSION"] withProcessEnv(
process.env["OPENCODE_PERMISSION"] = "{invalid" "OPENCODE_PERMISSION",
try { "{invalid",
await using tmp = await tmpdir() Effect.gen(function* () {
await withTestInstance({ const config = yield* Config.use.get()
directory: tmp.path, // Regression: load() used to throw before returning anything.
fn: async (ctx) => { expect(config).toBeDefined()
const config = await load(ctx) }),
// We don't assert on permission shape; the regression is that ),
// load() throws before returning anything. )
expect(config).toBeDefined()
},
})
} finally {
if (original !== undefined) {
process.env["OPENCODE_PERMISSION"] = original
} else {
delete process.env["OPENCODE_PERMISSION"]
}
}
})
}) })
describe("OPENCODE_CONFIG_CONTENT token substitution", () => { describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => { it.instance("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", () =>
const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] withProcessEnv(
const originalTestVar = process.env["TEST_CONFIG_VAR"] "TEST_CONFIG_VAR",
process.env["TEST_CONFIG_VAR"] = "test_api_key_12345" "test_api_key_12345",
process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ withProcessEnv(
$schema: "https://opencode.ai/config.json", "OPENCODE_CONFIG_CONTENT",
username: "{env:TEST_CONFIG_VAR}", JSON.stringify({
}) $schema: "https://opencode.ai/config.json",
username: "{env:TEST_CONFIG_VAR}",
try { }),
await using tmp = await tmpdir() Effect.gen(function* () {
await withTestInstance({ const config = yield* Config.use.get()
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.username).toBe("test_api_key_12345") 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 () => { it.instance("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", () =>
const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] Effect.gen(function* () {
const test = yield* TestInstance
try { yield* writeTextEffect(path.join(test.directory, "api_key.txt"), "secret_key_from_file")
await using tmp = await tmpdir({ yield* withProcessEnv(
init: async (dir) => { "OPENCODE_CONFIG_CONTENT",
await Filesystem.write(path.join(dir, "api_key.txt"), "secret_key_from_file") JSON.stringify({
process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ $schema: "https://opencode.ai/config.json",
$schema: "https://opencode.ai/config.json", username: "{file:./api_key.txt}",
username: "{file:./api_key.txt}", }),
}) Effect.gen(function* () {
}, const config = yield* Config.use.get()
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const config = await load(ctx)
expect(config.username).toBe("secret_key_from_file") 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 // parseManagedPlist unit tests — pure function, no OS interaction