test(opencode): use filesystem service accessors
This commit is contained in:
parent
22f79f05cf
commit
2fc0793f1f
2 changed files with 88 additions and 64 deletions
|
|
@ -7,6 +7,43 @@ import { Effect, FileSystem, Layer, Schema, Context } from "effect"
|
||||||
import type { PlatformError } from "effect/PlatformError"
|
import type { PlatformError } from "effect/PlatformError"
|
||||||
import { Glob } from "./util/glob"
|
import { Glob } from "./util/glob"
|
||||||
|
|
||||||
|
type EffectMethod = (...args: ReadonlyArray<never>) => Effect.Effect<unknown, unknown, unknown>
|
||||||
|
|
||||||
|
type ServiceUse<Identifier, Shape> = {
|
||||||
|
readonly [Key in keyof Shape as Shape[Key] extends EffectMethod ? Key : never]: Shape[Key] extends (
|
||||||
|
...args: infer Args
|
||||||
|
) => infer Return
|
||||||
|
? Args extends ReadonlyArray<unknown>
|
||||||
|
? Return extends Effect.Effect<infer A, infer E, infer R>
|
||||||
|
? (...args: Args) => Effect.Effect<A, E, R | Identifier>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, Shape>) => {
|
||||||
|
// This is the only dynamic boundary: TypeScript knows the accessor shape,
|
||||||
|
// but Proxy property names are runtime values.
|
||||||
|
const access = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_, key) => {
|
||||||
|
if (typeof key !== "string") return undefined
|
||||||
|
return (...args: unknown[]) =>
|
||||||
|
tag.use((service) => {
|
||||||
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime.
|
||||||
|
const method = service[key as keyof Shape]
|
||||||
|
if (typeof method !== "function") return Effect.die(new Error(`Service method not found: ${key}`))
|
||||||
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ServiceUse exposes only Effect-returning methods.
|
||||||
|
return (method as (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>)(...args)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy implements the mapped accessor surface lazily.
|
||||||
|
return access as ServiceUse<Identifier, Shape>
|
||||||
|
}
|
||||||
|
|
||||||
export namespace AppFileSystem {
|
export namespace AppFileSystem {
|
||||||
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
|
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
|
||||||
method: Schema.String,
|
method: Schema.String,
|
||||||
|
|
@ -39,6 +76,8 @@ export namespace AppFileSystem {
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
|
||||||
|
|
||||||
|
export const use = serviceUse(Service)
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -140,32 +140,21 @@ afterEach(async () => {
|
||||||
await clear(true)
|
await clear(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))
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeManagedSettingsEffect = (settings: object, filename?: string) =>
|
const writeManagedSettingsEffect = (settings: object, filename?: string) =>
|
||||||
Effect.promise(() => writeManagedSettings(settings, filename))
|
AppFileSystem.use.writeWithDirs(path.join(managedConfigDir, filename ?? "opencode.json"), JSON.stringify(settings))
|
||||||
|
|
||||||
async function writeConfig(dir: string, config: object, name = "opencode.json") {
|
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") =>
|
const writeConfigEffect = (dir: string, config: object, name = "opencode.json") =>
|
||||||
AppFileSystem.Service.use((fs) => fs.writeFileString(path.join(dir, name), JSON.stringify(config)))
|
AppFileSystem.use.writeFileString(path.join(dir, name), JSON.stringify(config))
|
||||||
const mkdirEffect = (dir: string) => AppFileSystem.Service.use((fs) => fs.ensureDir(dir))
|
|
||||||
const writeTextEffect = (file: string, content: string) =>
|
|
||||||
AppFileSystem.Service.use((fs) => fs.writeWithDirs(file, content))
|
|
||||||
const readTextEffect = (file: string) => AppFileSystem.Service.use((fs) => fs.readFileString(file))
|
|
||||||
const readJsonEffect = (file: string) => AppFileSystem.Service.use((fs) => fs.readJson(file))
|
|
||||||
const existsEffect = (file: string) => AppFileSystem.Service.use((fs) => fs.existsSafe(file))
|
|
||||||
const chmodEffect = (file: string, mode: number) => AppFileSystem.Service.use((fs) => fs.chmod(file, mode))
|
|
||||||
|
|
||||||
const project = {
|
const project = {
|
||||||
dir: (relative: string) => TestInstance.use((test) => mkdirEffect(path.join(test.directory, relative))),
|
dir: (relative: string) =>
|
||||||
|
TestInstance.use((test) => AppFileSystem.use.ensureDir(path.join(test.directory, relative))),
|
||||||
file: (relative: string, content: string) =>
|
file: (relative: string, content: string) =>
|
||||||
TestInstance.use((test) => writeTextEffect(path.join(test.directory, relative), content)),
|
TestInstance.use((test) => AppFileSystem.use.writeWithDirs(path.join(test.directory, relative), content)),
|
||||||
}
|
}
|
||||||
|
|
||||||
const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
|
const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
|
||||||
|
|
@ -202,7 +191,7 @@ const withConfigTree = <A, E, R>(
|
||||||
const root = yield* tmpdirScoped()
|
const root = yield* tmpdirScoped()
|
||||||
const directory = path.join(root, "project")
|
const directory = path.join(root, "project")
|
||||||
const local = path.join(directory, ".opencode")
|
const local = path.join(directory, ".opencode")
|
||||||
yield* mkdirEffect(local)
|
yield* AppFileSystem.use.ensureDir(local)
|
||||||
if (input.global) yield* writeConfigEffect(root, schemaConfig(input.global))
|
if (input.global) yield* writeConfigEffect(root, schemaConfig(input.global))
|
||||||
if (input.project) yield* writeConfigEffect(directory, schemaConfig(input.project))
|
if (input.project) yield* writeConfigEffect(directory, schemaConfig(input.project))
|
||||||
if (input.local) yield* writeConfigEffect(local, schemaConfig(input.local))
|
if (input.local) yield* writeConfigEffect(local, schemaConfig(input.local))
|
||||||
|
|
@ -302,7 +291,7 @@ it.effect("creates global jsonc config with schema when no global configs exist"
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Config.use.get().pipe(provideInstanceEffect(dir))
|
yield* Config.use.get().pipe(provideInstanceEffect(dir))
|
||||||
|
|
||||||
const content = yield* readTextEffect(path.join(dir, "opencode.jsonc"))
|
const content = yield* AppFileSystem.use.readFileString(path.join(dir, "opencode.jsonc"))
|
||||||
expect(content).toContain('"$schema": "https://opencode.ai/config.json"')
|
expect(content).toContain('"$schema": "https://opencode.ai/config.json"')
|
||||||
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
||||||
),
|
),
|
||||||
|
|
@ -318,7 +307,7 @@ it.effect("does not create global config when OPENCODE_CONFIG_DIR is set", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Config.use.get().pipe(provideInstanceEffect(dir))
|
yield* Config.use.get().pipe(provideInstanceEffect(dir))
|
||||||
|
|
||||||
expect(yield* existsEffect(path.join(dir, "opencode.jsonc"))).toBe(false)
|
expect(yield* AppFileSystem.use.existsSafe(path.join(dir, "opencode.jsonc"))).toBe(false)
|
||||||
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -355,10 +344,8 @@ it.instance("updates config and preserves empty shell sentinel", () =>
|
||||||
|
|
||||||
yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(Config.Info, { shell: "" }, "test:config")))
|
yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(Config.Info, { shell: "" }, "test:config")))
|
||||||
|
|
||||||
const writtenConfig = yield* Effect.promise(() =>
|
const writtenConfig = yield* AppFileSystem.use.readJson(path.join(test.directory, "config.json"))
|
||||||
Filesystem.readJson<{ shell?: string }>(path.join(test.directory, "config.json")),
|
expect(writtenConfig).toMatchObject({ shell: "" })
|
||||||
)
|
|
||||||
expect(writtenConfig.shell).toBe("")
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -367,7 +354,7 @@ it.effect("updates global config and omits empty shell key in json", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Config.use.updateGlobal({ shell: "" })
|
yield* Config.use.updateGlobal({ shell: "" })
|
||||||
|
|
||||||
const writtenConfig = yield* readJsonEffect(path.join(dir, "opencode.json"))
|
const writtenConfig = yield* AppFileSystem.use.readJson(path.join(dir, "opencode.json"))
|
||||||
expect(writtenConfig).not.toHaveProperty("shell")
|
expect(writtenConfig).not.toHaveProperty("shell")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -379,7 +366,7 @@ it.effect("updates global config and omits empty shell key in jsonc", () =>
|
||||||
yield* Config.use.updateGlobal({ shell: "" })
|
yield* Config.use.updateGlobal({ shell: "" })
|
||||||
|
|
||||||
const file = path.join(dir, "opencode.jsonc")
|
const file = path.join(dir, "opencode.jsonc")
|
||||||
const writtenConfig = yield* readTextEffect(file)
|
const writtenConfig = yield* AppFileSystem.use.readFileString(file)
|
||||||
const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(writtenConfig, file), file)
|
const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(writtenConfig, file), file)
|
||||||
expect(writtenConfig).not.toContain('"shell"')
|
expect(writtenConfig).not.toContain('"shell"')
|
||||||
expect(parsed.shell).toBeUndefined()
|
expect(parsed.shell).toBeUndefined()
|
||||||
|
|
@ -443,7 +430,7 @@ it.instance("ignores legacy tui keys in opencode config", () =>
|
||||||
it.instance("loads JSONC config file", () =>
|
it.instance("loads JSONC config file", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, "opencode.jsonc"),
|
path.join(test.directory, "opencode.jsonc"),
|
||||||
`{
|
`{
|
||||||
// This is a comment
|
// This is a comment
|
||||||
|
|
@ -503,7 +490,7 @@ it.instance("preserves env variables when adding $schema to config", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
// Config without $schema - should trigger auto-add
|
// Config without $schema - should trigger auto-add
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, "opencode.json"),
|
path.join(test.directory, "opencode.json"),
|
||||||
JSON.stringify({ username: "{env:PRESERVE_VAR}" }),
|
JSON.stringify({ username: "{env:PRESERVE_VAR}" }),
|
||||||
)
|
)
|
||||||
|
|
@ -511,7 +498,7 @@ it.instance("preserves env variables when adding $schema to config", () =>
|
||||||
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 = yield* readTextEffect(path.join(test.directory, "opencode.json"))
|
const content = yield* AppFileSystem.use.readFileString(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")
|
||||||
|
|
@ -522,7 +509,7 @@ it.instance("preserves env variables when adding $schema to config", () =>
|
||||||
it.instance("handles file inclusion substitution", () =>
|
it.instance("handles file inclusion substitution", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* writeTextEffect(path.join(test.directory, "included.txt"), "test-user")
|
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "included.txt"), "test-user")
|
||||||
yield* writeConfigEffect(test.directory, {
|
yield* writeConfigEffect(test.directory, {
|
||||||
$schema: "https://opencode.ai/config.json",
|
$schema: "https://opencode.ai/config.json",
|
||||||
username: "{file:included.txt}",
|
username: "{file:included.txt}",
|
||||||
|
|
@ -535,7 +522,7 @@ it.instance("handles file inclusion substitution", () =>
|
||||||
it.instance("handles file inclusion with replacement tokens", () =>
|
it.instance("handles file inclusion with replacement tokens", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* writeTextEffect(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`")
|
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`")
|
||||||
yield* writeConfigEffect(test.directory, {
|
yield* writeConfigEffect(test.directory, {
|
||||||
$schema: "https://opencode.ai/config.json",
|
$schema: "https://opencode.ai/config.json",
|
||||||
username: "{file:included.md}",
|
username: "{file:included.md}",
|
||||||
|
|
@ -603,7 +590,7 @@ it.instance("validates config schema and throws on invalid fields", () =>
|
||||||
it.instance("throws error for invalid JSON", () =>
|
it.instance("throws error for invalid JSON", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* writeTextEffect(path.join(test.directory, "opencode.json"), "{ invalid json }")
|
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "opencode.json"), "{ invalid json }")
|
||||||
const exit = yield* Config.use.get().pipe(Effect.exit)
|
const exit = yield* Config.use.get().pipe(Effect.exit)
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
}),
|
}),
|
||||||
|
|
@ -718,8 +705,8 @@ it.instance("migrates mode field to agent field", () =>
|
||||||
it.instance("loads config from .opencode directory", () =>
|
it.instance("loads config from .opencode directory", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode", "agent"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode", "agent"))
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "agent", "test.md"),
|
path.join(test.directory, ".opencode", "agent", "test.md"),
|
||||||
`---
|
`---
|
||||||
model: test/model
|
model: test/model
|
||||||
|
|
@ -741,8 +728,8 @@ Test agent prompt`,
|
||||||
it.instance("agent markdown permission config preserves user key order", () =>
|
it.instance("agent markdown permission config preserves user key order", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode", "agent"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode", "agent"))
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "agent", "ordered.md"),
|
path.join(test.directory, ".opencode", "agent", "ordered.md"),
|
||||||
`---
|
`---
|
||||||
permission:
|
permission:
|
||||||
|
|
@ -761,8 +748,8 @@ Ordered permissions`,
|
||||||
it.instance("loads agents from .opencode/agents (plural)", () =>
|
it.instance("loads agents from .opencode/agents (plural)", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode", "agents", "nested"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode", "agents", "nested"))
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "agents", "helper.md"),
|
path.join(test.directory, ".opencode", "agents", "helper.md"),
|
||||||
`---
|
`---
|
||||||
model: test/model
|
model: test/model
|
||||||
|
|
@ -771,7 +758,7 @@ mode: subagent
|
||||||
Helper agent prompt`,
|
Helper agent prompt`,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "agents", "nested", "child.md"),
|
path.join(test.directory, ".opencode", "agents", "nested", "child.md"),
|
||||||
`---
|
`---
|
||||||
model: test/model
|
model: test/model
|
||||||
|
|
@ -801,8 +788,8 @@ Nested agent prompt`,
|
||||||
it.instance("loads commands from .opencode/command (singular)", () =>
|
it.instance("loads commands from .opencode/command (singular)", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode", "command", "nested"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode", "command", "nested"))
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "command", "hello.md"),
|
path.join(test.directory, ".opencode", "command", "hello.md"),
|
||||||
`---
|
`---
|
||||||
description: Test command
|
description: Test command
|
||||||
|
|
@ -810,7 +797,7 @@ description: Test command
|
||||||
Hello from singular command`,
|
Hello from singular command`,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "command", "nested", "child.md"),
|
path.join(test.directory, ".opencode", "command", "nested", "child.md"),
|
||||||
`---
|
`---
|
||||||
description: Nested command
|
description: Nested command
|
||||||
|
|
@ -835,8 +822,8 @@ Nested command template`,
|
||||||
it.instance("loads commands from .opencode/commands (plural)", () =>
|
it.instance("loads commands from .opencode/commands (plural)", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode", "commands", "nested"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode", "commands", "nested"))
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "commands", "hello.md"),
|
path.join(test.directory, ".opencode", "commands", "hello.md"),
|
||||||
`---
|
`---
|
||||||
description: Test command
|
description: Test command
|
||||||
|
|
@ -844,7 +831,7 @@ description: Test command
|
||||||
Hello from plural commands`,
|
Hello from plural commands`,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "commands", "nested", "child.md"),
|
path.join(test.directory, ".opencode", "commands", "nested", "child.md"),
|
||||||
`---
|
`---
|
||||||
description: Nested command
|
description: Nested command
|
||||||
|
|
@ -873,10 +860,8 @@ it.instance("updates config and writes to file", () =>
|
||||||
svc.update(ConfigParse.schema(Config.Info, { model: "updated/model" }, "test:config")),
|
svc.update(ConfigParse.schema(Config.Info, { model: "updated/model" }, "test:config")),
|
||||||
)
|
)
|
||||||
|
|
||||||
const writtenConfig = yield* Effect.promise(() =>
|
const writtenConfig = yield* AppFileSystem.use.readJson(path.join(test.directory, "config.json"))
|
||||||
Filesystem.readJson<{ model: string }>(path.join(test.directory, "config.json")),
|
expect(writtenConfig).toMatchObject({ model: "updated/model" })
|
||||||
)
|
|
||||||
expect(writtenConfig.model).toBe("updated/model")
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -893,9 +878,9 @@ it.effect("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR
|
||||||
|
|
||||||
const dir = yield* tmpdirScoped()
|
const dir = yield* tmpdirScoped()
|
||||||
const readonly = path.join(dir, "readonly")
|
const readonly = path.join(dir, "readonly")
|
||||||
yield* mkdirEffect(readonly)
|
yield* AppFileSystem.use.ensureDir(readonly)
|
||||||
yield* chmodEffect(readonly, 0o555)
|
yield* AppFileSystem.use.chmod(readonly, 0o555)
|
||||||
yield* Effect.addFinalizer(() => chmodEffect(readonly, 0o755).pipe(Effect.ignore))
|
yield* Effect.addFinalizer(() => AppFileSystem.use.chmod(readonly, 0o755).pipe(Effect.ignore))
|
||||||
|
|
||||||
yield* withProcessEnv("OPENCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
|
yield* withProcessEnv("OPENCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
|
||||||
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
||||||
|
|
@ -905,7 +890,7 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = yield* tmpdirScoped()
|
const dir = yield* tmpdirScoped()
|
||||||
const configDir = path.join(dir, "configdir")
|
const configDir = path.join(dir, "configdir")
|
||||||
yield* mkdirEffect(configDir)
|
yield* AppFileSystem.use.ensureDir(configDir)
|
||||||
|
|
||||||
yield* withProcessEnv(
|
yield* withProcessEnv(
|
||||||
"OPENCODE_CONFIG_DIR",
|
"OPENCODE_CONFIG_DIR",
|
||||||
|
|
@ -915,7 +900,7 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* readTextEffect(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
|
expect(yield* AppFileSystem.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
|
||||||
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -927,12 +912,12 @@ it.instance("resolves scoped npm plugins in config", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
const pluginDir = path.join(test.directory, "node_modules", "@scope", "plugin")
|
const pluginDir = path.join(test.directory, "node_modules", "@scope", "plugin")
|
||||||
yield* mkdirEffect(pluginDir)
|
yield* AppFileSystem.use.ensureDir(pluginDir)
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, "package.json"),
|
path.join(test.directory, "package.json"),
|
||||||
JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
|
JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
|
||||||
)
|
)
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(pluginDir, "package.json"),
|
path.join(pluginDir, "package.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
|
|
@ -945,7 +930,7 @@ it.instance("resolves scoped npm plugins in config", () =>
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
yield* writeTextEffect(path.join(pluginDir, "index.js"), "export default {}\n")
|
yield* AppFileSystem.use.writeWithDirs(path.join(pluginDir, "index.js"), "export default {}\n")
|
||||||
yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] })
|
yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] })
|
||||||
|
|
||||||
const config = yield* Config.use.get()
|
const config = yield* Config.use.get()
|
||||||
|
|
@ -975,8 +960,8 @@ it.effect("merges plugin arrays from global and local configs", () =>
|
||||||
it.instance("does not error when only custom agent is a subagent", () =>
|
it.instance("does not error when only custom agent is a subagent", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode", "agent"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode", "agent"))
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "agent", "helper.md"),
|
path.join(test.directory, ".opencode", "agent", "helper.md"),
|
||||||
`---
|
`---
|
||||||
model: test/model
|
model: test/model
|
||||||
|
|
@ -1387,7 +1372,7 @@ it.instance("local .opencode config can override MCP from project config", () =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode"))
|
||||||
yield* writeConfigEffect(
|
yield* writeConfigEffect(
|
||||||
path.join(test.directory, ".opencode"),
|
path.join(test.directory, ".opencode"),
|
||||||
{
|
{
|
||||||
|
|
@ -1719,8 +1704,8 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
||||||
"true",
|
"true",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* mkdirEffect(path.join(test.directory, ".opencode", "command"))
|
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode", "command"))
|
||||||
yield* writeTextEffect(
|
yield* AppFileSystem.use.writeWithDirs(
|
||||||
path.join(test.directory, ".opencode", "command", "test-cmd.md"),
|
path.join(test.directory, ".opencode", "command", "test-cmd.md"),
|
||||||
"# Test Command\nThis is a test command.",
|
"# Test Command\nThis is a test command.",
|
||||||
)
|
)
|
||||||
|
|
@ -1749,7 +1734,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
||||||
{ OPENCODE_CONFIG_DIR: undefined, OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
|
{ OPENCODE_CONFIG_DIR: undefined, OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* writeTextEffect(path.join(test.directory, "CUSTOM.md"), "# Custom Instructions")
|
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "CUSTOM.md"), "# Custom Instructions")
|
||||||
// The relative instruction should be skipped without error
|
// The relative instruction should be skipped without error
|
||||||
const config = yield* Config.use.get()
|
const config = yield* Config.use.get()
|
||||||
expect(config).toBeDefined()
|
expect(config).toBeDefined()
|
||||||
|
|
@ -1814,7 +1799,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
|
||||||
it.instance("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", () =>
|
it.instance("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
yield* writeTextEffect(path.join(test.directory, "api_key.txt"), "secret_key_from_file")
|
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "api_key.txt"), "secret_key_from_file")
|
||||||
yield* withProcessEnv(
|
yield* withProcessEnv(
|
||||||
"OPENCODE_CONFIG_CONTENT",
|
"OPENCODE_CONFIG_CONTENT",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue