refactor(instance): remove legacy runtime fallback (#27757)
This commit is contained in:
parent
9975c1ed1c
commit
0c9cfe923f
47 changed files with 939 additions and 1256 deletions
|
|
@ -116,7 +116,7 @@ describe("my service", () => {
|
|||
Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a manual runtime in each test.
|
||||
|
||||
- `tmpdirScoped(options?)` creates a scoped temp directory and cleans it up when the Effect scope closes.
|
||||
- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it just runs an Effect with `Instance.current` bound to `dir`.
|
||||
- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it runs an Effect with `InstanceRef` provided for `dir`.
|
||||
- `provideTmpdirInstance((dir) => effect, options?)` is the convenience helper. It creates a temp directory, binds it as the active instance, and disposes the instance on cleanup.
|
||||
- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|||
import { Effect } from "effect"
|
||||
import { fileURLToPath } from "url"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
|
|
@ -13,46 +12,28 @@ afterEach(async () => {
|
|||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
// Regression for PR #25522: when an effectCmd handler does
|
||||
// `yield* Effect.promise(async () => { ... await runPromise(svcMethod) ... })`,
|
||||
// the inner runPromise creates a fresh fiber after `await` whose Effect context
|
||||
// has lost the outer InstanceRef. Services that read `InstanceState.context`
|
||||
// then fall back to `Instance.current` ALS, which must be installed at the JS
|
||||
// callback boundary (Node ALS persists across awaits, Effect's fiber context
|
||||
// does not). `it.instance` provides the loaded InstanceRef; the explicit
|
||||
// Instance.restore mirrors effectCmd's load + ALS-restore wrap.
|
||||
// Pins effect-cmd.ts directly: the pattern test below exercises the load +
|
||||
// Instance.restore boundary via the shared `it.instance` fixture,
|
||||
// so a regression that removed `Instance.restore` from effect-cmd.ts wouldn't
|
||||
// fail it. This grep guards the actual production callsite.
|
||||
it.live("effect-cmd.ts wraps the handler body in Instance.restore", () =>
|
||||
it.live("effect-cmd.ts does not restore legacy instance ALS", () =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const source = yield* fs.readFileString(fileURLToPath(new URL("../../src/cli/effect-cmd.ts", import.meta.url)))
|
||||
expect(source).toContain("Instance.restore(ctx")
|
||||
expect(source).not.toContain("restore(ctx")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"Instance.current reachable after await inside restored Effect.promise(async)",
|
||||
"InstanceRef remains the handler context across Effect promise awaits",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const ctx = yield* InstanceRef
|
||||
if (!ctx) throw new Error("InstanceRef not provided")
|
||||
|
||||
const current = yield* Effect.promise(() =>
|
||||
Instance.restore(ctx, async () => {
|
||||
await Promise.resolve()
|
||||
try {
|
||||
return Instance.current
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}),
|
||||
)
|
||||
const directory = yield* Effect.promise(async () => {
|
||||
await Promise.resolve()
|
||||
return ctx.directory
|
||||
})
|
||||
|
||||
expect(current?.directory).toBe(test.directory)
|
||||
expect(directory).toBe(test.directory)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { ConfigManaged } from "@/config/managed"
|
|||
import { ConfigParse } from "../../src/config/parse"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Account } from "../../src/account/account"
|
||||
|
|
@ -61,9 +62,20 @@ const layer = Config.layer.pipe(
|
|||
|
||||
const it = testEffect(layer)
|
||||
|
||||
const load = () => Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
const save = (config: Config.Info) =>
|
||||
Effect.runPromise(Config.Service.use((svc) => svc.update(config)).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
const provideCurrentInstance = <A, E, R>(effect: Effect.Effect<A, E, R>, ctx: InstanceContext) =>
|
||||
effect.pipe(Effect.provideService(InstanceRef, ctx))
|
||||
|
||||
const load = (ctx: InstanceContext) =>
|
||||
Effect.runPromise(
|
||||
Config.Service.use((svc) => provideCurrentInstance(svc.get(), ctx)).pipe(Effect.scoped, Effect.provide(layer)),
|
||||
)
|
||||
const save = (config: Config.Info, ctx: InstanceContext) =>
|
||||
Effect.runPromise(
|
||||
Config.Service.use((svc) => provideCurrentInstance(svc.update(config), ctx)).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
)
|
||||
const saveGlobal = (config: Config.Info) =>
|
||||
Effect.runPromise(
|
||||
Config.Service.use((svc) => svc.updateGlobal(config)).pipe(
|
||||
|
|
@ -76,10 +88,20 @@ const clear = async (wait = false) => {
|
|||
await Effect.runPromise(Config.Service.use((svc) => svc.invalidate()).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
if (wait) await InstanceRuntime.disposeAllInstances()
|
||||
}
|
||||
const listDirs = () =>
|
||||
Effect.runPromise(Config.Service.use((svc) => svc.directories()).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
const ready = () =>
|
||||
Effect.runPromise(Config.Service.use((svc) => svc.waitForDependencies()).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
const listDirs = (ctx: InstanceContext) =>
|
||||
Effect.runPromise(
|
||||
Config.Service.use((svc) => provideCurrentInstance(svc.directories(), ctx)).pipe(
|
||||
Effect.scoped,
|
||||
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)
|
||||
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
|
||||
|
|
@ -116,11 +138,11 @@ async function check(map: (dir: string) => string) {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: map(tmp.path),
|
||||
fn: async () => {
|
||||
const cfg = await load()
|
||||
fn: async (ctx) => {
|
||||
const cfg = await load(ctx)
|
||||
expect(cfg.snapshot).toBe(true)
|
||||
expect(Instance.directory).toBe(Filesystem.resolve(tmp.path))
|
||||
expect(Instance.project.id).not.toBe(ProjectID.global)
|
||||
expect(ctx.directory).toBe(Filesystem.resolve(tmp.path))
|
||||
expect(ctx.project.id).not.toBe(ProjectID.global)
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
|
|
@ -134,8 +156,8 @@ test("loads config with defaults when no files exist", async () => {
|
|||
await using tmp = await tmpdir()
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.username).toBeDefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -150,8 +172,8 @@ test("creates global jsonc config with schema when no global configs exist", asy
|
|||
try {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await load()
|
||||
fn: async (ctx) => {
|
||||
await load(ctx)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -175,8 +197,8 @@ test("does not create global config when OPENCODE_CONFIG_DIR is set", async () =
|
|||
try {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await load()
|
||||
fn: async (ctx) => {
|
||||
await load(ctx)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -201,8 +223,8 @@ test("loads JSON config file", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.model).toBe("test/model")
|
||||
expect(config.username).toBe("testuser")
|
||||
},
|
||||
|
|
@ -220,8 +242,8 @@ test("loads shell config field", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.shell).toBe("bash")
|
||||
},
|
||||
})
|
||||
|
|
@ -242,8 +264,8 @@ test("updates config and preserves empty shell sentinel", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await save({ shell: "" })
|
||||
fn: async (ctx) => {
|
||||
await save({ shell: "" }, ctx)
|
||||
|
||||
const writtenConfig = await Filesystem.readJson<{ shell?: string }>(path.join(tmp.path, "config.json"))
|
||||
expect(writtenConfig.shell).toBe("")
|
||||
|
|
@ -320,8 +342,8 @@ test("loads formatter boolean config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.formatter).toBe(true)
|
||||
},
|
||||
})
|
||||
|
|
@ -338,8 +360,8 @@ test("loads lsp boolean config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.lsp).toBe(true)
|
||||
},
|
||||
})
|
||||
|
|
@ -375,8 +397,8 @@ test("ignores legacy tui keys in opencode config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.model).toBe("test/model")
|
||||
expect((config as Record<string, unknown>).theme).toBeUndefined()
|
||||
expect((config as Record<string, unknown>).tui).toBeUndefined()
|
||||
|
|
@ -400,8 +422,8 @@ test("loads JSONC config file", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.model).toBe("test/model")
|
||||
expect(config.username).toBe("testuser")
|
||||
},
|
||||
|
|
@ -428,8 +450,8 @@ test("jsonc overrides json in the same directory", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.model).toBe("base")
|
||||
expect(config.username).toBe("base")
|
||||
},
|
||||
|
|
@ -451,8 +473,8 @@ test("handles environment variable substitution", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.username).toBe("test-user")
|
||||
},
|
||||
})
|
||||
|
|
@ -483,8 +505,8 @@ test("preserves env variables when adding $schema to config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.username).toBe("secret_value")
|
||||
|
||||
// Read the file to verify the env variable was preserved
|
||||
|
|
@ -580,8 +602,8 @@ test("handles file inclusion substitution", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.username).toBe("test-user")
|
||||
},
|
||||
})
|
||||
|
|
@ -599,8 +621,8 @@ test("handles file inclusion with replacement tokens", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.username).toBe("const out = await Bun.$`echo hi`")
|
||||
},
|
||||
})
|
||||
|
|
@ -617,9 +639,9 @@ test("validates config schema and throws on invalid fields", async () => {
|
|||
})
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
// Strict schema should throw an error for invalid fields
|
||||
await expect(load()).rejects.toThrow()
|
||||
await expect(load(ctx)).rejects.toThrow()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -632,8 +654,8 @@ test("throws error for invalid JSON", async () => {
|
|||
})
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(load()).rejects.toThrow()
|
||||
fn: async (ctx) => {
|
||||
await expect(load(ctx)).rejects.toThrow()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -655,8 +677,8 @@ test("handles agent configuration", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test_agent"]).toEqual(
|
||||
expect.objectContaining({
|
||||
model: "test/model",
|
||||
|
|
@ -686,8 +708,8 @@ test("treats agent variant as model-scoped setting (not provider option)", async
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
const agent = config.agent?.["test_agent"]
|
||||
|
||||
expect(agent?.variant).toBe("xhigh")
|
||||
|
|
@ -716,8 +738,8 @@ test("handles command configuration", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.command?.["test_command"]).toEqual({
|
||||
template: "test template",
|
||||
description: "test command",
|
||||
|
|
@ -741,8 +763,8 @@ test("migrates autoshare to share field", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.share).toBe("auto")
|
||||
expect(config.autoshare).toBe(true)
|
||||
},
|
||||
|
|
@ -768,8 +790,8 @@ test("migrates mode field to agent field", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test_mode"]).toEqual({
|
||||
model: "test/model",
|
||||
temperature: 0.5,
|
||||
|
|
@ -800,8 +822,8 @@ Test agent prompt`,
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "test",
|
||||
|
|
@ -833,8 +855,8 @@ Ordered permissions`,
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"])
|
||||
},
|
||||
})
|
||||
|
|
@ -871,8 +893,8 @@ Nested agent prompt`,
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
|
||||
expect(config.agent?.["helper"]).toMatchObject({
|
||||
name: "helper",
|
||||
|
|
@ -920,8 +942,8 @@ Nested command template`,
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
|
||||
expect(config.command?.["hello"]).toEqual({
|
||||
description: "Test command",
|
||||
|
|
@ -965,8 +987,8 @@ Nested command template`,
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
|
||||
expect(config.command?.["hello"]).toEqual({
|
||||
description: "Test command",
|
||||
|
|
@ -985,9 +1007,9 @@ test("updates config and writes to file", async () => {
|
|||
await using tmp = await tmpdir()
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const newConfig = { model: "updated/model" }
|
||||
await save(newConfig as any)
|
||||
await save(newConfig as any, ctx)
|
||||
|
||||
const writtenConfig = await Filesystem.readJson<{ model: string }>(path.join(tmp.path, "config.json"))
|
||||
expect(writtenConfig.model).toBe("updated/model")
|
||||
|
|
@ -999,8 +1021,8 @@ test("gets config directories", async () => {
|
|||
await using tmp = await tmpdir()
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const dirs = await listDirs()
|
||||
fn: async (ctx) => {
|
||||
const dirs = await listDirs(ctx)
|
||||
expect(dirs.length).toBeGreaterThanOrEqual(1)
|
||||
},
|
||||
})
|
||||
|
|
@ -1029,8 +1051,8 @@ test("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR", as
|
|||
try {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await load()
|
||||
fn: async (ctx) => {
|
||||
await load(ctx)
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
|
|
@ -1064,10 +1086,18 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
|
|||
try {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(testLayer)))
|
||||
fn: async (ctx) => {
|
||||
await Effect.runPromise(
|
||||
Config.Service.use((svc) => svc.waitForDependencies()).pipe(Effect.scoped, Effect.provide(testLayer)),
|
||||
Config.Service.use((svc) => svc.get().pipe(Effect.provideService(InstanceRef, ctx))).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(testLayer),
|
||||
),
|
||||
)
|
||||
await Effect.runPromise(
|
||||
Config.Service.use((svc) => svc.waitForDependencies().pipe(Effect.provideService(InstanceRef, ctx))).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(testLayer),
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -1123,8 +1153,8 @@ test("resolves scoped npm plugins in config", async () => {
|
|||
|
||||
await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
const pluginEntries = config.plugin ?? []
|
||||
expect(pluginEntries).toContain("@scope/plugin")
|
||||
},
|
||||
|
|
@ -1161,8 +1191,8 @@ test("merges plugin arrays from global and local configs", async () => {
|
|||
|
||||
await provideTestInstance({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
const plugins = config.plugin ?? []
|
||||
|
||||
// Should contain both global and local plugins
|
||||
|
|
@ -1197,8 +1227,8 @@ Helper subagent prompt`,
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["helper"]).toMatchObject({
|
||||
name: "helper",
|
||||
model: "test/model",
|
||||
|
|
@ -1236,8 +1266,8 @@ test("merges instructions arrays from global and local configs", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
const instructions = config.instructions ?? []
|
||||
|
||||
expect(instructions).toContain("global-instructions.md")
|
||||
|
|
@ -1275,8 +1305,8 @@ test("deduplicates duplicate instructions from global and local configs", async
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
const instructions = config.instructions ?? []
|
||||
|
||||
expect(instructions).toContain("global-only.md")
|
||||
|
|
@ -1320,8 +1350,8 @@ test("deduplicates duplicate plugins from global and local configs", async () =>
|
|||
|
||||
await provideTestInstance({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
const plugins = config.plugin ?? []
|
||||
|
||||
// Should contain all unique plugins
|
||||
|
|
@ -1369,8 +1399,8 @@ test("keeps plugin origins aligned with merged plugin list", async () => {
|
|||
|
||||
await provideTestInstance({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const cfg = await load()
|
||||
fn: async (ctx) => {
|
||||
const cfg = await load(ctx)
|
||||
const plugins = cfg.plugin ?? []
|
||||
const origins = cfg.plugin_origins ?? []
|
||||
const names = plugins.map((item) => ConfigPlugin.pluginSpecifier(item))
|
||||
|
|
@ -1410,8 +1440,8 @@ test("migrates legacy tools config to permissions - allow", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
bash: "allow",
|
||||
read: "allow",
|
||||
|
|
@ -1441,8 +1471,8 @@ test("migrates legacy tools config to permissions - deny", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
bash: "deny",
|
||||
webfetch: "deny",
|
||||
|
|
@ -1471,8 +1501,8 @@ test("migrates legacy write tool to edit permission", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
edit: "allow",
|
||||
})
|
||||
|
|
@ -1503,8 +1533,8 @@ test("managed settings override user settings", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.model).toBe("managed/model")
|
||||
expect(config.share).toBe("disabled")
|
||||
expect(config.username).toBe("testuser")
|
||||
|
|
@ -1531,8 +1561,8 @@ test("managed settings override project settings", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.autoupdate).toBe(false)
|
||||
expect(config.disabled_providers).toEqual(["openai"])
|
||||
},
|
||||
|
|
@ -1551,8 +1581,8 @@ test("missing managed settings file is not an error", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.model).toBe("user/model")
|
||||
},
|
||||
})
|
||||
|
|
@ -1578,8 +1608,8 @@ test("migrates legacy edit tool to edit permission", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
edit: "deny",
|
||||
})
|
||||
|
|
@ -1607,8 +1637,8 @@ test("migrates legacy patch tool to edit permission", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
edit: "allow",
|
||||
})
|
||||
|
|
@ -1639,8 +1669,8 @@ test("migrates mixed legacy tools config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
bash: "allow",
|
||||
edit: "allow",
|
||||
|
|
@ -1674,8 +1704,8 @@ test("merges legacy tools with existing permission config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.agent?.["test"]?.permission).toEqual({
|
||||
glob: "allow",
|
||||
bash: "allow",
|
||||
|
|
@ -1711,8 +1741,8 @@ test("permission config preserves user key order", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(Object.keys(config.permission!)).toEqual([
|
||||
"*",
|
||||
"edit",
|
||||
|
|
@ -1794,8 +1824,8 @@ test("project config can override MCP server enabled status", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
// jira should be enabled (overridden by project config)
|
||||
expect(config.mcp?.jira).toEqual({
|
||||
type: "remote",
|
||||
|
|
@ -1850,8 +1880,8 @@ test("MCP config deep merges preserving base config properties", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.mcp?.myserver).toEqual({
|
||||
type: "remote",
|
||||
url: "https://myserver.example.com/mcp",
|
||||
|
|
@ -1901,8 +1931,8 @@ test("local .opencode config can override MCP from project config", async () =>
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.mcp?.docs?.enabled).toBe(true)
|
||||
},
|
||||
})
|
||||
|
|
@ -2235,8 +2265,8 @@ describe("deduplicatePluginOrigins", () => {
|
|||
|
||||
await provideTestInstance({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
const plugins = config.plugin ?? []
|
||||
|
||||
expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true)
|
||||
|
|
@ -2267,8 +2297,8 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
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.username).not.toBe("project-user")
|
||||
|
|
@ -2298,8 +2328,8 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const directories = await listDirs()
|
||||
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)
|
||||
|
|
@ -2322,9 +2352,9 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
await using tmp = await tmpdir()
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
// Should still get default config (from global or defaults)
|
||||
const config = await load()
|
||||
const config = await load(ctx)
|
||||
expect(config).toBeDefined()
|
||||
expect(config.username).toBeDefined()
|
||||
},
|
||||
|
|
@ -2364,10 +2394,10 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
// The relative instruction should be skipped without error
|
||||
// We're mainly verifying this doesn't throw and the config loads
|
||||
const config = await load()
|
||||
const config = await load(ctx)
|
||||
expect(config).toBeDefined()
|
||||
// The instruction should have been skipped (warning logged)
|
||||
// We can't easily test the warning was logged, but we verify
|
||||
|
|
@ -2424,8 +2454,8 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: projectTmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
// Should load from OPENCODE_CONFIG_DIR, not project
|
||||
expect(config.model).toBe("configdir/model")
|
||||
},
|
||||
|
|
@ -2459,8 +2489,8 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
|
|||
await using tmp = await tmpdir()
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.username).toBe("test_api_key_12345")
|
||||
},
|
||||
})
|
||||
|
|
@ -2493,8 +2523,8 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
fn: async (ctx) => {
|
||||
const config = await load(ctx)
|
||||
expect(config.username).toBe("secret_key_from_file")
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -139,7 +139,18 @@ async function initGitRepo(dir: string) {
|
|||
await $`git commit -m "base"`.cwd(dir).quiet()
|
||||
}
|
||||
|
||||
const runWorkspace = <A, E>(effect: Effect.Effect<A, E, Workspace.Service>) => AppRuntime.runPromise(effect)
|
||||
function currentInstance() {
|
||||
try {
|
||||
return context.use()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const runWorkspace = <A, E>(effect: Effect.Effect<A, E, Workspace.Service>) => {
|
||||
const ctx = currentInstance()
|
||||
return AppRuntime.runPromise(ctx ? effect.pipe(Effect.provideService(InstanceRef, ctx)) : effect)
|
||||
}
|
||||
const createWorkspace = (input: Workspace.CreateInput) =>
|
||||
runWorkspace(Workspace.Service.use((workspace) => workspace.create(input)))
|
||||
const warpWorkspaceSession = (input: Workspace.SessionWarpInput) =>
|
||||
|
|
@ -917,7 +928,9 @@ describe("workspace CRUD", () => {
|
|||
const previous = workspaceInfo(projectID, previousType)
|
||||
insertWorkspace(previous)
|
||||
registerAdapter(projectID, previousType, localAdapter(workspaceTmp.path, { createDir: false }).adapter)
|
||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
const session = await AppRuntime.runPromise(
|
||||
SessionNs.Service.use((svc) => svc.create({})).pipe(Effect.provideService(InstanceRef, instance)),
|
||||
)
|
||||
attachSessionToWorkspace(session.id, previous.id)
|
||||
|
||||
const workspaceCtx = await AppRuntime.runPromise(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|||
import { $ } from "bun"
|
||||
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|||
import type { Config } from "@/config/config"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { TestLLMServer } from "../lib/llm-server"
|
||||
|
||||
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||
|
|
@ -24,11 +24,15 @@ const testInstanceRuntime = ManagedRuntime.make(
|
|||
const runTestInstanceStore = <A>(fn: (store: InstanceStore.Interface) => Effect.Effect<A>) =>
|
||||
testInstanceRuntime.runPromise(InstanceStore.Service.use(fn))
|
||||
|
||||
export async function provideTestInstance<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) {
|
||||
export async function provideTestInstance<R>(input: {
|
||||
directory: string
|
||||
init?: Effect.Effect<void>
|
||||
fn: (ctx: InstanceContext) => R
|
||||
}) {
|
||||
const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory }))
|
||||
try {
|
||||
if (input.init) await testInstanceRuntime.runPromise(input.init.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
return await Instance.restore(ctx, () => input.fn())
|
||||
return await input.fn(ctx)
|
||||
} finally {
|
||||
await runTestInstanceStore((store) => store.dispose(ctx))
|
||||
}
|
||||
|
|
@ -157,9 +161,7 @@ export const provideInstance =
|
|||
Effect.contextWith((services: Context.Context<R>) =>
|
||||
Effect.promise<A>(async () => {
|
||||
const ctx = await runTestInstanceStore((store) => store.load({ directory }))
|
||||
return Instance.restore(ctx, () =>
|
||||
Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, ctx))),
|
||||
)
|
||||
return Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { pathToFileURL } from "url"
|
|||
import { tmpdir } from "../fixture/fixture"
|
||||
import { LSPClient } from "@/lsp/client"
|
||||
import * as LSPServer from "@/lsp/server"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
|
|
@ -28,12 +27,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
directory: process.cwd(),
|
||||
instance: ctx,
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -51,12 +51,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
directory: process.cwd(),
|
||||
instance: ctx,
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -74,12 +75,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
directory: process.cwd(),
|
||||
instance: ctx,
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -97,12 +99,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
directory: process.cwd(),
|
||||
instance: ctx,
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -124,7 +127,7 @@ describe("LSPClient interop", () => {
|
|||
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
fn: (ctx) =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: {
|
||||
|
|
@ -133,6 +136,7 @@ describe("LSPClient interop", () => {
|
|||
},
|
||||
root: process.cwd(),
|
||||
directory: process.cwd(),
|
||||
instance: ctx,
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -153,12 +157,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
directory: tmp.path,
|
||||
instance: ctx,
|
||||
})
|
||||
|
||||
await client.notify.open({ path: file })
|
||||
|
|
@ -196,12 +201,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
directory: tmp.path,
|
||||
instance: ctx,
|
||||
})
|
||||
|
||||
const version = await client.notify.open({ path: file })
|
||||
|
|
@ -242,12 +248,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
directory: tmp.path,
|
||||
instance: ctx,
|
||||
})
|
||||
|
||||
const version = await client.notify.open({ path: file })
|
||||
|
|
@ -289,12 +296,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
directory: tmp.path,
|
||||
instance: ctx,
|
||||
})
|
||||
|
||||
await client.connection.sendRequest("test/configure-pull-diagnostics", {
|
||||
|
|
@ -337,12 +345,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
directory: tmp.path,
|
||||
instance: ctx,
|
||||
})
|
||||
|
||||
await client.connection.sendRequest("test/configure-pull-diagnostics", {
|
||||
|
|
@ -390,12 +399,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
directory: tmp.path,
|
||||
instance: ctx,
|
||||
})
|
||||
|
||||
await client.connection.sendRequest("test/configure-pull-diagnostics", {
|
||||
|
|
@ -454,12 +464,13 @@ describe("LSPClient interop", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
const client = await LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: tmp.path,
|
||||
directory: tmp.path,
|
||||
instance: ctx,
|
||||
})
|
||||
|
||||
await client.connection.sendRequest("test/configure-pull-diagnostics", {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
|||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { Plugin } from "../../src/plugin/index"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { Vcs } from "../../src/project/vcs"
|
||||
import { InstanceState } from "../../src/effect/instance-state"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
|
|
@ -116,11 +116,12 @@ describe("plugin.workspace", () => {
|
|||
const plugin = yield* Plugin.Service
|
||||
yield* plugin.init()
|
||||
const workspace = yield* Workspace.Service
|
||||
const ctx = yield* InstanceState.context
|
||||
const info = yield* workspace.create({
|
||||
type,
|
||||
branch: null,
|
||||
extra: { key: "value" },
|
||||
projectID: Instance.project.id,
|
||||
projectID: ctx.project.id,
|
||||
})
|
||||
|
||||
expect(info.type).toBe(type)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import { Deferred, Effect, Fiber, Layer } from "effect"
|
|||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { registerDisposer } from "../../src/effect/instance-registry"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { TestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
let bootstrapRun: Effect.Effect<void> = Effect.void
|
||||
|
|
@ -37,7 +36,7 @@ const registerDisposerScoped = (disposer: (directory: string) => Promise<void>)
|
|||
)
|
||||
|
||||
describe("InstanceStore", () => {
|
||||
it.live("loads instance context without installing ALS for the caller", () =>
|
||||
it.live("loads instance context", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const store = yield* InstanceStore.Service
|
||||
|
|
@ -45,7 +44,6 @@ describe("InstanceStore", () => {
|
|||
|
||||
expect(ctx.directory).toBe(dir)
|
||||
expect(ctx.worktree).toBe(dir)
|
||||
expect(() => Instance.current).toThrow()
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -63,7 +61,6 @@ describe("InstanceStore", () => {
|
|||
yield* store.load({ directory: dir })
|
||||
|
||||
expect(initializedDirectory).toBe(dir)
|
||||
expect(() => Instance.current).toThrow()
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -245,20 +242,4 @@ describe("InstanceStore", () => {
|
|||
expect(disposed).toEqual([dir1, dir2])
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"provides legacy Promise callers with instance ALS",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const ctx = yield* InstanceRef
|
||||
if (!ctx) throw new Error("InstanceRef not provided")
|
||||
|
||||
const directory = yield* Effect.promise(() => Promise.resolve(Instance.restore(ctx, () => Instance.directory)))
|
||||
|
||||
expect(directory).toBe(test.directory)
|
||||
expect(() => Instance.current).toThrow()
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|||
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
|
||||
import { Git } from "../../src/git"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture"
|
||||
|
|
@ -41,7 +41,10 @@ const waitReady = Effect.fn("WorktreeTest.waitReady")(function* () {
|
|||
const removeCreatedWorktree = (directory: string) =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const ctx = yield* Effect.sync(() => Instance.current).pipe(provideInstance(directory))
|
||||
const ctx = yield* Effect.gen(function* () {
|
||||
return yield* InstanceRef
|
||||
}).pipe(provideInstance(directory))
|
||||
if (!ctx) return yield* Effect.die(new Error("missing test instance"))
|
||||
yield* Effect.promise(() => InstanceRuntime.disposeInstance(ctx))
|
||||
const ok = yield* svc.remove({ directory })
|
||||
if (!ok) return yield* Effect.fail(new Error(`failed to remove worktree ${directory}`))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { test, expect, describe } from "bun:test"
|
||||
import { afterEach, test, expect, describe } from "bun:test"
|
||||
import path from "path"
|
||||
import { unlink } from "fs/promises"
|
||||
|
||||
import { ProviderID } from "../../src/provider/schema"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Env } from "../../src/env"
|
||||
|
|
@ -12,17 +12,37 @@ import { Global } from "@opencode-ai/core/global"
|
|||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Effect } from "effect"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
|
||||
const env = makeRuntime(Env.Service, Env.defaultLayer)
|
||||
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
|
||||
const originalEnv = new Map<string, string | undefined>()
|
||||
|
||||
async function list() {
|
||||
function rememberEnv(k: string) {
|
||||
if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
|
||||
}
|
||||
|
||||
const set = (ctx: InstanceContext, k: string, v: string) => {
|
||||
rememberEnv(k)
|
||||
process.env[k] = v
|
||||
return env.runSync((svc) => svc.set(k, v).pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const [key, value] of originalEnv) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
originalEnv.clear()
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
async function list(ctx: InstanceContext) {
|
||||
return AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
return yield* provider.list()
|
||||
}),
|
||||
}).pipe(Effect.provideService(InstanceRef, ctx)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -46,10 +66,10 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async ()
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_REGION", "us-east-1")
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_REGION", "us-east-1")
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
|
|
@ -69,10 +89,10 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async ()
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_REGION", "eu-west-1")
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_REGION", "eu-west-1")
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
|
|
@ -122,11 +142,11 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "")
|
||||
set("AWS_ACCESS_KEY_ID", "")
|
||||
set("AWS_BEARER_TOKEN_BEDROCK", "")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_PROFILE", "")
|
||||
set(ctx, "AWS_ACCESS_KEY_ID", "")
|
||||
set(ctx, "AWS_BEARER_TOKEN_BEDROCK", "")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
|
|
@ -166,10 +186,10 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
set("AWS_ACCESS_KEY_ID", "test-key-id")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
set(ctx, "AWS_ACCESS_KEY_ID", "test-key-id")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
|
||||
},
|
||||
|
|
@ -196,9 +216,9 @@ test("Bedrock: includes custom endpoint in options when specified", async () =>
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe(
|
||||
"https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
|
||||
|
|
@ -227,12 +247,12 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async ()
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
|
||||
set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
|
||||
set("AWS_PROFILE", "")
|
||||
set("AWS_ACCESS_KEY_ID", "")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
|
||||
set(ctx, "AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
|
||||
set(ctx, "AWS_PROFILE", "")
|
||||
set(ctx, "AWS_ACCESS_KEY_ID", "")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
|
||||
},
|
||||
|
|
@ -268,9 +288,9 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () =>
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
// The model should exist with the us. prefix
|
||||
expect(providers[ProviderID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
||||
|
|
@ -303,9 +323,9 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
||||
},
|
||||
|
|
@ -337,9 +357,9 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () =>
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
||||
},
|
||||
|
|
@ -371,9 +391,9 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "AWS_PROFILE", "default")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
// Non-prefixed model should still be registered
|
||||
expect(providers[ProviderID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export {}
|
|||
|
||||
// import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
// import { tmpdir } from "../fixture/fixture"
|
||||
// import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
// import { Provider } from "@/provider/provider"
|
||||
// import { Env } from "../../src/env"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { afterEach, test, expect } from "bun:test"
|
||||
import { mkdir, unlink } from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Plugin } from "../../src/plugin/index"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
|
|
@ -14,6 +14,7 @@ import { Filesystem } from "@/util/filesystem"
|
|||
import { Env } from "../../src/env"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
|
|
@ -22,8 +23,31 @@ import { Auth } from "@/auth"
|
|||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
|
||||
const env = makeRuntime(Env.Service, Env.defaultLayer)
|
||||
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
|
||||
const remove = (k: string) => env.runSync((svc) => svc.remove(k))
|
||||
const originalEnv = new Map<string, string | undefined>()
|
||||
|
||||
function rememberEnv(k: string) {
|
||||
if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
|
||||
}
|
||||
|
||||
const set = (ctx: InstanceContext, k: string, v: string) => {
|
||||
rememberEnv(k)
|
||||
process.env[k] = v
|
||||
return env.runSync((svc) => svc.set(k, v).pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
}
|
||||
const remove = (ctx: InstanceContext, k: string) => {
|
||||
rememberEnv(k)
|
||||
delete process.env[k]
|
||||
return env.runSync((svc) => svc.remove(k).pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const [key, value] of originalEnv) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
originalEnv.clear()
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
Provider.layer.pipe(
|
||||
|
|
@ -36,41 +60,41 @@ const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
|||
Layer.provide(RuntimeFlags.layer(flags)),
|
||||
)
|
||||
|
||||
async function run<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
|
||||
async function run<A, E>(ctx: InstanceContext, fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
|
||||
return AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
return yield* fn(provider)
|
||||
}),
|
||||
}).pipe(Effect.provideService(InstanceRef, ctx)),
|
||||
)
|
||||
}
|
||||
|
||||
async function list() {
|
||||
return run((provider) => provider.list())
|
||||
async function list(ctx: InstanceContext) {
|
||||
return run(ctx, (provider) => provider.list())
|
||||
}
|
||||
|
||||
async function getProvider(providerID: ProviderID) {
|
||||
return run((provider) => provider.getProvider(providerID))
|
||||
async function getProvider(providerID: ProviderID, ctx: InstanceContext) {
|
||||
return run(ctx, (provider) => provider.getProvider(providerID))
|
||||
}
|
||||
|
||||
async function getModel(providerID: ProviderID, modelID: ModelID) {
|
||||
return run((provider) => provider.getModel(providerID, modelID))
|
||||
async function getModel(providerID: ProviderID, modelID: ModelID, ctx: InstanceContext) {
|
||||
return run(ctx, (provider) => provider.getModel(providerID, modelID))
|
||||
}
|
||||
|
||||
async function getLanguage(model: Provider.Model) {
|
||||
return run((provider) => provider.getLanguage(model))
|
||||
async function getLanguage(model: Provider.Model, ctx: InstanceContext) {
|
||||
return run(ctx, (provider) => provider.getLanguage(model))
|
||||
}
|
||||
|
||||
async function closest(providerID: ProviderID, query: string[]) {
|
||||
return run((provider) => provider.closest(providerID, query))
|
||||
async function closest(providerID: ProviderID, query: string[], ctx: InstanceContext) {
|
||||
return run(ctx, (provider) => provider.closest(providerID, query))
|
||||
}
|
||||
|
||||
async function getSmallModel(providerID: ProviderID) {
|
||||
return run((provider) => provider.getSmallModel(providerID))
|
||||
async function getSmallModel(providerID: ProviderID, ctx: InstanceContext) {
|
||||
return run(ctx, (provider) => provider.getSmallModel(providerID))
|
||||
}
|
||||
|
||||
async function defaultModel() {
|
||||
return run((provider) => provider.defaultModel())
|
||||
async function defaultModel(ctx: InstanceContext) {
|
||||
return run(ctx, (provider) => provider.defaultModel())
|
||||
}
|
||||
|
||||
async function markPluginDependenciesReady(dir: string) {
|
||||
|
|
@ -125,9 +149,9 @@ test("provider loaded from env variable", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
// Provider should retain its connection source even if custom loaders
|
||||
// merge additional options.
|
||||
|
|
@ -157,8 +181,8 @@ test("provider loaded from config with apiKey option", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -178,9 +202,9 @@ test("disabled_providers excludes provider", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -200,10 +224,10 @@ test("enabled_providers restricts to only listed providers", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
set(ctx, "OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
},
|
||||
|
|
@ -228,9 +252,9 @@ test("model whitelist filters models for provider", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
expect(models).toContain("claude-sonnet-4-20250514")
|
||||
|
|
@ -257,9 +281,9 @@ test("model blacklist excludes specific models", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
expect(models).not.toContain("claude-sonnet-4-20250514")
|
||||
|
|
@ -290,9 +314,9 @@ test("custom model alias via config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].models["my-alias"].name).toBe("My Custom Alias")
|
||||
|
|
@ -334,8 +358,8 @@ test("custom provider with npm package", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("custom-provider")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("custom-provider")].name).toBe("Custom Provider")
|
||||
expect(providers[ProviderID.make("custom-provider")].models["custom-model"]).toBeDefined()
|
||||
|
|
@ -411,8 +435,8 @@ test("custom DeepSeek openai-compatible model defaults interleaved reasoning fie
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
const provider = providers[ProviderID.make("custom-provider")]
|
||||
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
|
||||
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
|
||||
|
|
@ -445,9 +469,9 @@ test("env variable takes precedence, config merges options", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "env-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "env-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
// Config options should be merged
|
||||
expect(providers[ProviderID.anthropic].options.timeout).toBe(60000)
|
||||
|
|
@ -469,13 +493,13 @@ test("getModel returns model for valid provider/model", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
|
||||
expect(model).toBeDefined()
|
||||
expect(String(model.providerID)).toBe("anthropic")
|
||||
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
|
||||
const language = await getLanguage(model)
|
||||
const language = await getLanguage(model, ctx)
|
||||
expect(language).toBeDefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -494,9 +518,9 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"), ctx)).rejects.toThrow()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -514,8 +538,8 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
|
||||
fn: async (ctx) => {
|
||||
expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"), ctx)).rejects.toThrow()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -545,9 +569,9 @@ test("defaultModel returns first available model when no config set", async () =
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await defaultModel()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await defaultModel(ctx)
|
||||
expect(model.providerID).toBeDefined()
|
||||
expect(model.modelID).toBeDefined()
|
||||
},
|
||||
|
|
@ -568,9 +592,9 @@ test("defaultModel respects config model setting", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await defaultModel()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await defaultModel(ctx)
|
||||
expect(String(model.providerID)).toBe("anthropic")
|
||||
expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
|
||||
},
|
||||
|
|
@ -701,9 +725,9 @@ test("closest finds model by partial match", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const result = await closest(ProviderID.anthropic, ["sonnet-4"])
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const result = await closest(ProviderID.anthropic, ["sonnet-4"], ctx)
|
||||
expect(result).toBeDefined()
|
||||
expect(String(result?.providerID)).toBe("anthropic")
|
||||
expect(String(result?.modelID)).toContain("sonnet-4")
|
||||
|
|
@ -724,8 +748,8 @@ test("closest returns undefined for nonexistent provider", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await closest(ProviderID.make("nonexistent"), ["model"])
|
||||
fn: async (ctx) => {
|
||||
const result = await closest(ProviderID.make("nonexistent"), ["model"], ctx)
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -754,12 +778,12 @@ test("getModel uses realIdByKey for aliased models", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
|
||||
|
||||
const model = await getModel(ProviderID.anthropic, ModelID.make("my-sonnet"))
|
||||
const model = await getModel(ProviderID.anthropic, ModelID.make("my-sonnet"), ctx)
|
||||
expect(model).toBeDefined()
|
||||
expect(String(model.id)).toBe("my-sonnet")
|
||||
expect(model.name).toBe("My Sonnet Alias")
|
||||
|
|
@ -798,8 +822,8 @@ test("provider api field sets model api.url", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
// api field is stored on model.api.url, used by getSDK to set baseURL
|
||||
expect(providers[ProviderID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1")
|
||||
},
|
||||
|
|
@ -838,8 +862,8 @@ test("explicit baseURL overrides api field", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
|
||||
},
|
||||
})
|
||||
|
|
@ -867,9 +891,9 @@ test("model inherits properties from existing database model", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.name).toBe("Custom Name for Sonnet")
|
||||
expect(model.capabilities.toolcall).toBe(true)
|
||||
|
|
@ -893,9 +917,9 @@ test("disabled_providers prevents loading even with env var", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -915,10 +939,10 @@ test("enabled_providers with empty array allows no providers", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
set(ctx, "OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list(ctx)
|
||||
expect(Object.keys(providers).length).toBe(0)
|
||||
},
|
||||
})
|
||||
|
|
@ -943,9 +967,9 @@ test("whitelist and blacklist can be combined", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
expect(models).toContain("claude-sonnet-4-20250514")
|
||||
|
|
@ -983,8 +1007,8 @@ test("model modalities default correctly", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.make("test-provider")].models["test-model"]
|
||||
expect(model.capabilities.input.text).toBe(true)
|
||||
expect(model.capabilities.output.text).toBe(true)
|
||||
|
|
@ -1026,8 +1050,8 @@ test("model with custom cost values", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.make("test-provider")].models["test-model"]
|
||||
expect(model.cost.input).toBe(5)
|
||||
expect(model.cost.output).toBe(15)
|
||||
|
|
@ -1050,9 +1074,9 @@ test("getSmallModel returns appropriate small model", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getSmallModel(ProviderID.anthropic)
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getSmallModel(ProviderID.anthropic, ctx)
|
||||
expect(model).toBeDefined()
|
||||
expect(model?.id).toContain("haiku")
|
||||
},
|
||||
|
|
@ -1073,9 +1097,9 @@ test("getSmallModel respects config small_model override", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getSmallModel(ProviderID.anthropic)
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getSmallModel(ProviderID.anthropic, ctx)
|
||||
expect(model).toBeDefined()
|
||||
expect(String(model?.providerID)).toBe("anthropic")
|
||||
expect(String(model?.id)).toBe("claude-sonnet-4-20250514")
|
||||
|
|
@ -1097,9 +1121,9 @@ test("getSmallModel ignores invalid config small_model", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
expect(await getSmallModel(ProviderID.anthropic)).toBeUndefined()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
expect(await getSmallModel(ProviderID.anthropic, ctx)).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -1140,10 +1164,10 @@ test("multiple providers can be configured simultaneously", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set(ctx, "OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
|
||||
|
|
@ -1183,8 +1207,8 @@ test("provider with custom npm package", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("local-llm")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("local-llm")].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
|
||||
expect(providers[ProviderID.make("local-llm")].options.baseURL).toBe("http://localhost:11434/v1")
|
||||
|
|
@ -1217,9 +1241,9 @@ test("model alias name defaults to alias key when id differs", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
|
||||
},
|
||||
})
|
||||
|
|
@ -1255,9 +1279,9 @@ test("provider with multiple env var options only includes apiKey when single en
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("MULTI_ENV_KEY_1", "test-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "MULTI_ENV_KEY_1", "test-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("multi-env")]).toBeDefined()
|
||||
// When multiple env options exist, key should NOT be auto-set
|
||||
expect(providers[ProviderID.make("multi-env")].key).toBeUndefined()
|
||||
|
|
@ -1295,9 +1319,9 @@ test("provider with single env var includes apiKey automatically", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("SINGLE_ENV_KEY", "my-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "SINGLE_ENV_KEY", "my-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("single-env")]).toBeDefined()
|
||||
// Single env option should auto-set key
|
||||
expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key")
|
||||
|
|
@ -1330,9 +1354,9 @@ test("model cost overrides existing cost values", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.cost.input).toBe(999)
|
||||
expect(model.cost.output).toBe(888)
|
||||
|
|
@ -1378,8 +1402,8 @@ test("completely new provider not in database can be configured", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("brand-new-provider")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("brand-new-provider")].name).toBe("Brand New")
|
||||
const model = providers[ProviderID.make("brand-new-provider")].models["new-model"]
|
||||
|
|
@ -1407,11 +1431,11 @@ test("disabled_providers and enabled_providers interaction", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic")
|
||||
set("OPENAI_API_KEY", "test-openai")
|
||||
set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-anthropic")
|
||||
set(ctx, "OPENAI_API_KEY", "test-openai")
|
||||
set(ctx, "GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
|
||||
const providers = await list(ctx)
|
||||
// anthropic: in enabled, not in disabled = allowed
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
// openai: in enabled, but also in disabled = NOT allowed
|
||||
|
|
@ -1450,8 +1474,8 @@ test("model with tool_call false", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
|
||||
},
|
||||
})
|
||||
|
|
@ -1485,8 +1509,8 @@ test("model defaults tool_call to true when not specified", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
|
||||
},
|
||||
})
|
||||
|
|
@ -1524,8 +1548,8 @@ test("model headers are preserved", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.make("headers-provider")].models["model"]
|
||||
expect(model.headers).toEqual({
|
||||
"X-Custom-Header": "custom-value",
|
||||
|
|
@ -1563,10 +1587,10 @@ test("provider env fallback - second env var used if first missing", async () =>
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
fn: async (ctx) => {
|
||||
// Only set fallback, not primary
|
||||
set("FALLBACK_KEY", "fallback-api-key")
|
||||
const providers = await list()
|
||||
set(ctx, "FALLBACK_KEY", "fallback-api-key")
|
||||
const providers = await list(ctx)
|
||||
// Provider should load because fallback env var is set
|
||||
expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
|
||||
},
|
||||
|
|
@ -1586,10 +1610,10 @@ test("getModel returns consistent results", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
|
||||
const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
|
||||
expect(model1.providerID).toEqual(model2.providerID)
|
||||
expect(model1.id).toEqual(model2.id)
|
||||
expect(model1).toEqual(model2)
|
||||
|
|
@ -1625,8 +1649,8 @@ test("provider name defaults to id when not in database", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id")
|
||||
},
|
||||
})
|
||||
|
|
@ -1645,10 +1669,10 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
try {
|
||||
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
|
||||
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4"), ctx) // typo: sonet instead of sonnet
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.suggestions).toBeDefined()
|
||||
|
|
@ -1671,10 +1695,10 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
try {
|
||||
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
|
||||
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4"), ctx) // typo: antropic
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.suggestions).toBeDefined()
|
||||
|
|
@ -1697,10 +1721,10 @@ test("ModelNotFoundError suggests catalog models for unloaded providers", async
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
remove("OPENCODE_API_KEY")
|
||||
fn: async (ctx) => {
|
||||
remove(ctx, "OPENCODE_API_KEY")
|
||||
try {
|
||||
await getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model"))
|
||||
await getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model"), ctx)
|
||||
throw new Error("expected model lookup to fail")
|
||||
} catch (e) {
|
||||
if (!Provider.ModelNotFoundError.isInstance(e)) throw e
|
||||
|
|
@ -1723,8 +1747,8 @@ test("getProvider returns undefined for nonexistent provider", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const provider = await getProvider(ProviderID.make("nonexistent"))
|
||||
fn: async (ctx) => {
|
||||
const provider = await getProvider(ProviderID.make("nonexistent"), ctx)
|
||||
expect(provider).toBeUndefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -1743,9 +1767,9 @@ test("getProvider returns provider info", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const provider = await getProvider(ProviderID.anthropic)
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const provider = await getProvider(ProviderID.anthropic, ctx)
|
||||
expect(provider).toBeDefined()
|
||||
expect(String(provider?.id)).toBe("anthropic")
|
||||
},
|
||||
|
|
@ -1765,9 +1789,9 @@ test("closest returns undefined when no partial match found", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"], ctx)
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -1786,10 +1810,10 @@ test("closest checks multiple query terms in order", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
// First term won't match, second will
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"])
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"], ctx)
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.modelID).toContain("haiku")
|
||||
},
|
||||
|
|
@ -1824,8 +1848,8 @@ test("model limit defaults to zero when not specified", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.make("no-limit")].models["model"]
|
||||
expect(model.limit.context).toBe(0)
|
||||
expect(model.limit.output).toBe(0)
|
||||
|
|
@ -1856,9 +1880,9 @@ test("provider options are deeply merged", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
// Custom options should be merged
|
||||
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
|
||||
expect(providers[ProviderID.anthropic].options.headers["X-Custom"]).toBe("custom-value")
|
||||
|
|
@ -1888,8 +1912,8 @@ test("hosted nvidia provider adds billing origin header", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
|
|
@ -1920,8 +1944,8 @@ test("custom nvidia baseURL adds billing origin header", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
|
|
@ -1955,8 +1979,8 @@ test("explicit nvidia billing origin header is preserved", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin")
|
||||
},
|
||||
})
|
||||
|
|
@ -1986,9 +2010,9 @@ test("custom model inherits npm package from models.dev provider config", async
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "OPENAI_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.openai].models["my-custom-model"]
|
||||
expect(model).toBeDefined()
|
||||
expect(model.api.npm).toBe("@ai-sdk/openai")
|
||||
|
|
@ -2019,9 +2043,9 @@ test("custom model inherits api.url from models.dev provider", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("OPENROUTER_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "OPENROUTER_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.openrouter]).toBeDefined()
|
||||
|
||||
// New model not in database should inherit api.url from provider
|
||||
|
|
@ -2150,9 +2174,9 @@ test("model variants are generated for reasoning models", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
// Claude sonnet 4 has reasoning capability
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.capabilities.reasoning).toBe(true)
|
||||
|
|
@ -2186,9 +2210,9 @@ test("model variants can be disabled via config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants).toBeDefined()
|
||||
expect(model.variants!["high"]).toBeUndefined()
|
||||
|
|
@ -2227,9 +2251,9 @@ test("model variants can be customized via config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants!["high"]).toBeDefined()
|
||||
expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
|
||||
|
|
@ -2264,9 +2288,9 @@ test("disabled key is stripped from variant config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants!["max"]).toBeDefined()
|
||||
expect(model.variants!["max"].disabled).toBeUndefined()
|
||||
|
|
@ -2300,9 +2324,9 @@ test("all variants can be disabled via config", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants).toBeDefined()
|
||||
expect(Object.keys(model.variants!).length).toBe(0)
|
||||
|
|
@ -2336,9 +2360,9 @@ test("variant config merges with generated variants", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants!["high"]).toBeDefined()
|
||||
// Should have both the generated thinking config and the custom option
|
||||
|
|
@ -2372,9 +2396,9 @@ test("variants filtered in second pass for database models", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "OPENAI_API_KEY", "test-api-key")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.openai].models["gpt-5"]
|
||||
expect(model.variants).toBeDefined()
|
||||
expect(model.variants!["high"]).toBeUndefined()
|
||||
|
|
@ -2419,8 +2443,8 @@ test("custom model with variants enabled and disabled", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"]
|
||||
expect(model.variants).toBeDefined()
|
||||
// Enabled variants should exist
|
||||
|
|
@ -2474,9 +2498,9 @@ test("Google Vertex: retains baseURL for custom proxy", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
|
||||
},
|
||||
|
|
@ -2517,9 +2541,9 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
const providers = await list(ctx)
|
||||
const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
|
||||
|
||||
expect(model).toBeDefined()
|
||||
|
|
@ -2541,11 +2565,11 @@ test("cloudflare-ai-gateway loads with env variables", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set("CLOUDFLARE_API_TOKEN", "test-token")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
||||
},
|
||||
})
|
||||
|
|
@ -2571,11 +2595,11 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
|
|||
})
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set("CLOUDFLARE_API_TOKEN", "test-token")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
|
||||
invoked_by: "test",
|
||||
|
|
@ -2624,14 +2648,14 @@ test("plugin config providers persist after instance dispose", async () => {
|
|||
|
||||
const first = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
fn: async (ctx) =>
|
||||
AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const provider = yield* Provider.Service
|
||||
yield* plugin.init()
|
||||
return yield* provider.list()
|
||||
}),
|
||||
}).pipe(Effect.provideService(InstanceRef, ctx)),
|
||||
),
|
||||
})
|
||||
expect(first[ProviderID.make("demo")]).toBeDefined()
|
||||
|
|
@ -2641,7 +2665,7 @@ test("plugin config providers persist after instance dispose", async () => {
|
|||
|
||||
const second = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => list(),
|
||||
fn: async (ctx) => list(ctx),
|
||||
})
|
||||
expect(second[ProviderID.make("demo")]).toBeDefined()
|
||||
expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
|
||||
|
|
@ -2672,10 +2696,10 @@ test("plugin config enabled and disabled providers are honored", async () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list()
|
||||
fn: async (ctx) => {
|
||||
set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set(ctx, "OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list(ctx)
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
},
|
||||
|
|
@ -2696,7 +2720,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
|
|||
|
||||
const none = await WithInstance.provide({
|
||||
directory: base.path,
|
||||
fn: async () => paid(await list()),
|
||||
fn: async (ctx) => paid(await list(ctx)),
|
||||
})
|
||||
|
||||
await using keyed = await tmpdir({
|
||||
|
|
@ -2719,7 +2743,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
|
|||
|
||||
const keyedCount = await WithInstance.provide({
|
||||
directory: keyed.path,
|
||||
fn: async () => paid(await list()),
|
||||
fn: async (ctx) => paid(await list(ctx)),
|
||||
})
|
||||
|
||||
expect(none).toBe(0)
|
||||
|
|
@ -2740,7 +2764,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
|
|||
|
||||
const none = await WithInstance.provide({
|
||||
directory: base.path,
|
||||
fn: async () => paid(await list()),
|
||||
fn: async (ctx) => paid(await list(ctx)),
|
||||
})
|
||||
|
||||
await using keyed = await tmpdir({
|
||||
|
|
@ -2774,7 +2798,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
|
|||
|
||||
const keyedCount = await WithInstance.provide({
|
||||
directory: keyed.path,
|
||||
fn: async () => paid(await list()),
|
||||
fn: async (ctx) => paid(await list(ctx)),
|
||||
})
|
||||
|
||||
expect(none).toBe(0)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { afterEach, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Fiber, Layer, Queue } from "effect"
|
||||
import { Question } from "../../src/question"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { QuestionID } from "../../src/question/schema"
|
||||
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
|
|
@ -404,7 +404,10 @@ it.live("pending question rejects on instance dispose", () =>
|
|||
}).pipe(provideInstance(dir), Effect.forkScoped)
|
||||
|
||||
expect(yield* waitForPending(1).pipe(provideInstance(dir))).toHaveLength(1)
|
||||
const ctx = yield* Effect.sync(() => Instance.current).pipe(provideInstance(dir))
|
||||
const ctx = yield* Effect.gen(function* () {
|
||||
return yield* InstanceRef
|
||||
}).pipe(provideInstance(dir))
|
||||
if (!ctx) return yield* Effect.die(new Error("missing test instance"))
|
||||
yield* Effect.promise(() => InstanceRuntime.disposeInstance(ctx))
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
|
||||
import { Event as ServerEvent } from "../../src/server/event"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, reloadTestInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
|
|
@ -108,7 +109,9 @@ describe("event HttpApi", () => {
|
|||
|
||||
const next = readEvent(reader)
|
||||
const ctx = await reloadTestInstance({ directory: tmp.path })
|
||||
await Instance.restore(ctx, () => Bus.publish(ServerEvent.Connected, {}))
|
||||
await AppRuntime.runPromise(
|
||||
Bus.Service.use((svc) => svc.publish(ServerEvent.Connected, {})).pipe(Effect.provideService(InstanceRef, ctx)),
|
||||
)
|
||||
|
||||
expect(await next).toMatchObject({ type: "server.connected", properties: {} })
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export type Runtime = {
|
|||
HttpApiApp: (typeof import("../../../src/server/routes/instance/httpapi/server"))["HttpApiApp"]
|
||||
AppLayer: (typeof import("../../../src/effect/app-runtime"))["AppLayer"]
|
||||
InstanceRef: (typeof import("../../../src/effect/instance-ref"))["InstanceRef"]
|
||||
Instance: (typeof import("../../../src/project/instance"))["Instance"]
|
||||
InstanceStore: (typeof import("../../../src/project/instance-store"))["InstanceStore"]
|
||||
Session: (typeof import("../../../src/session/session"))["Session"]
|
||||
Todo: (typeof import("../../../src/session/todo"))["Todo"]
|
||||
|
|
@ -23,7 +22,6 @@ export function runtime() {
|
|||
const httpApiServer = await import("../../../src/server/routes/instance/httpapi/server")
|
||||
const appRuntime = await import("../../../src/effect/app-runtime")
|
||||
const instanceRef = await import("../../../src/effect/instance-ref")
|
||||
const instance = await import("../../../src/project/instance")
|
||||
const instanceStore = await import("../../../src/project/instance-store")
|
||||
const session = await import("../../../src/session/session")
|
||||
const todo = await import("../../../src/session/todo")
|
||||
|
|
@ -37,7 +35,6 @@ export function runtime() {
|
|||
HttpApiApp: httpApiServer.HttpApiApp,
|
||||
AppLayer: appRuntime.AppLayer,
|
||||
InstanceRef: instanceRef.InstanceRef,
|
||||
Instance: instance.Instance,
|
||||
InstanceStore: instanceStore.InstanceStore,
|
||||
Session: session.Session,
|
||||
Todo: todo.Todo,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Context } from "effect"
|
|||
import path from "path"
|
||||
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { FilePaths } from "../../src/server/routes/instance/httpapi/groups/file"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { WorkspaceID } from "../../src/control-plane/schema"
|
|||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceLayer } from "../../src/project/instance-layer"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||
import { PtyID } from "../../src/pty/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Layer } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
|
||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
||||
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import * as Log from "@opencode-ai/core/util/log"
|
|||
import { Server } from "../../src/server/server"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Project } from "../../src/project/project"
|
||||
|
|
@ -71,7 +70,7 @@ function listedAdapter(directory: string, type: string): WorkspaceAdapter {
|
|||
},
|
||||
async create() {},
|
||||
async remove() {},
|
||||
list() {
|
||||
list(context) {
|
||||
return [
|
||||
{
|
||||
type,
|
||||
|
|
@ -79,7 +78,7 @@ function listedAdapter(directory: string, type: string): WorkspaceAdapter {
|
|||
branch: "listed/main",
|
||||
directory,
|
||||
extra: { listed: true },
|
||||
projectID: Instance.project.id,
|
||||
projectID: context?.instance?.project.id ?? missingAdapterContext(),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
|
@ -92,6 +91,10 @@ function listedAdapter(directory: string, type: string): WorkspaceAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
function missingAdapterContext(): never {
|
||||
throw new Error("missing workspace adapter context")
|
||||
}
|
||||
|
||||
function remoteAdapter(directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter {
|
||||
return {
|
||||
name: "Remote Test",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { tool, type ModelMessage } from "ai"
|
|||
import { Cause, Effect, Exit, Stream } from "effect"
|
||||
import z from "zod"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
|
|
@ -18,19 +19,21 @@ import { MessageV2 } from "../../src/session/message-v2"
|
|||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
|
||||
async function getModel(providerID: ProviderID, modelID: ModelID) {
|
||||
return AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
return yield* provider.getModel(providerID, modelID)
|
||||
}),
|
||||
)
|
||||
async function getModel(providerID: ProviderID, modelID: ModelID, ctx: InstanceContext) {
|
||||
const effect = Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
return yield* provider.getModel(providerID, modelID)
|
||||
})
|
||||
return AppRuntime.runPromise(effect.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
}
|
||||
|
||||
const llm = makeRuntime(LLM.Service, LLM.defaultLayer)
|
||||
|
||||
async function drain(input: LLM.StreamInput) {
|
||||
return llm.runPromise((svc) => svc.stream(input).pipe(Stream.runDrain))
|
||||
async function drain(input: LLM.StreamInput, ctx: InstanceContext) {
|
||||
return llm.runPromise((svc) => {
|
||||
const effect = svc.stream(input).pipe(Stream.runDrain)
|
||||
return effect.pipe(Effect.provideService(InstanceRef, ctx))
|
||||
})
|
||||
}
|
||||
|
||||
describe("session.llm.hasToolCalls", () => {
|
||||
|
|
@ -360,8 +363,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-1")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -381,15 +384,18 @@ describe("session.llm.stream", () => {
|
|||
model: { providerID: ProviderID.make(providerID), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
})
|
||||
await drain(
|
||||
{
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
const capture = await request
|
||||
const body = capture.body
|
||||
|
|
@ -447,8 +453,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-service-abort")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -478,7 +484,7 @@ describe("session.llm.stream", () => {
|
|||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
})
|
||||
.pipe(Stream.runDrain),
|
||||
.pipe(Stream.runDrain, Effect.provideService(InstanceRef, ctx)),
|
||||
{ signal: ctrl.signal },
|
||||
)
|
||||
|
||||
|
|
@ -537,8 +543,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-tools")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -557,22 +563,25 @@ describe("session.llm.stream", () => {
|
|||
tools: { question: true },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
permission: [{ permission: "question", pattern: "*", action: "allow" }],
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {
|
||||
question: tool({
|
||||
description: "Ask a question",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => ({ output: "" }),
|
||||
}),
|
||||
await drain(
|
||||
{
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
permission: [{ permission: "question", pattern: "*", action: "allow" }],
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {
|
||||
question: tool({
|
||||
description: "Ask a question",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => ({ output: "" }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx,
|
||||
)
|
||||
|
||||
const capture = await request
|
||||
const tools = capture.body.tools as Array<{ function?: { name?: string } }> | undefined
|
||||
|
|
@ -651,8 +660,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-2")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -671,15 +680,18 @@ describe("session.llm.stream", () => {
|
|||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
})
|
||||
await drain(
|
||||
{
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
const capture = await request
|
||||
const body = capture.body
|
||||
|
|
@ -767,8 +779,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-data-url")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -786,28 +798,31 @@ describe("session.llm.stream", () => {
|
|||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this image" },
|
||||
{
|
||||
type: "file",
|
||||
mediaType: "image/png",
|
||||
filename: "large-image.png",
|
||||
data: image,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as ModelMessage[],
|
||||
tools: {},
|
||||
})
|
||||
await drain(
|
||||
{
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this image" },
|
||||
{
|
||||
type: "file",
|
||||
mediaType: "image/png",
|
||||
filename: "large-image.png",
|
||||
data: image,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as ModelMessage[],
|
||||
tools: {},
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
const capture = await request
|
||||
expect(capture.url.pathname.endsWith("/responses")).toBe(true)
|
||||
|
|
@ -886,8 +901,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-3")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -907,15 +922,18 @@ describe("session.llm.stream", () => {
|
|||
model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.5") },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
})
|
||||
await drain(
|
||||
{
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
const capture = await request
|
||||
const body = capture.body
|
||||
|
|
@ -1004,8 +1022,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-anthropic-tools")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -1110,31 +1128,34 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
] as any[]
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: [],
|
||||
messages: await MessageV2.toModelMessages(input as any, resolved),
|
||||
tools: {
|
||||
read: tool({
|
||||
description: "Stub read tool",
|
||||
inputSchema: z.object({
|
||||
filePath: z.string(),
|
||||
await drain(
|
||||
{
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: [],
|
||||
messages: await MessageV2.toModelMessages(input as any, resolved),
|
||||
tools: {
|
||||
read: tool({
|
||||
description: "Stub read tool",
|
||||
inputSchema: z.object({
|
||||
filePath: z.string(),
|
||||
}),
|
||||
execute: async () => ({ output: "stub" }),
|
||||
}),
|
||||
execute: async () => ({ output: "stub" }),
|
||||
}),
|
||||
glob: tool({
|
||||
description: "Stub glob tool",
|
||||
inputSchema: z.object({
|
||||
pattern: z.string(),
|
||||
path: z.string().optional(),
|
||||
glob: tool({
|
||||
description: "Stub glob tool",
|
||||
inputSchema: z.object({
|
||||
pattern: z.string(),
|
||||
path: z.string().optional(),
|
||||
}),
|
||||
execute: async () => ({ output: "stub" }),
|
||||
}),
|
||||
execute: async () => ({ output: "stub" }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx,
|
||||
)
|
||||
|
||||
const capture = await request
|
||||
const body = capture.body
|
||||
|
|
@ -1245,8 +1266,8 @@ describe("session.llm.stream", () => {
|
|||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
fn: async (ctx) => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
|
||||
const sessionID = SessionID.make("session-test-4")
|
||||
const agent = {
|
||||
name: "test",
|
||||
|
|
@ -1266,15 +1287,18 @@ describe("session.llm.stream", () => {
|
|||
model: { providerID: ProviderID.make(providerID), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
})
|
||||
await drain(
|
||||
{
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {},
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
const capture = await request
|
||||
const body = capture.body
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("no-ops for paths inside Instance.directory", () =>
|
||||
it.live("no-ops for paths inside the instance directory", () =>
|
||||
provideInstance("/tmp/project")(
|
||||
Effect.gen(function* () {
|
||||
const { requests, ctx } = makeCtx()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { Tool } from "@/tool/tool"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
|||
import { Git } from "@/git"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
import { ReadTool } from "../../src/tool/read"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import path from "path"
|
|||
import { pathToFileURL } from "url"
|
||||
import type { Permission } from "../../src/permission"
|
||||
import type { Tool } from "@/tool/tool"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { SkillTool } from "../../src/tool/skill"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Effect, Layer } from "effect"
|
|||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { WriteTool } from "../../src/tool/write"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Bus } from "../../src/bus"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue