feat(sdk): add ConfigProvider seam to embedded OpenCode hosts
This commit is contained in:
parent
651c23ba1d
commit
a25ba777ef
2 changed files with 40 additions and 20 deletions
|
|
@ -4,14 +4,30 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
import { ConfigProvider, Context, Effect, Layer, Scope } from "effect"
|
||||
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
|
||||
export const create = Effect.fn("OpenCode.create")(function* () {
|
||||
export interface Options {
|
||||
/**
|
||||
* Replaces the ConfigProvider this host's layers read while they are built,
|
||||
* for example the OPENCODE_DB database placement. The default provider
|
||||
* snapshots the process environment on first use, so per-host configuration
|
||||
* must come through this seam rather than env mutation. Compose with
|
||||
* ConfigProvider.orElse(ConfigProvider.fromEnv()) to keep environment
|
||||
* fallback.
|
||||
*/
|
||||
readonly configProvider?: ConfigProvider.ConfigProvider
|
||||
}
|
||||
|
||||
export const create = Effect.fn("OpenCode.create")(function* (options?: Options) {
|
||||
const scope = yield* Scope.Scope
|
||||
const memoMap = yield* Layer.makeMemoMap
|
||||
const withConfig = <A, E, R>(layer: Layer.Layer<A, E, R>) =>
|
||||
options?.configProvider === undefined
|
||||
? layer
|
||||
: layer.pipe(Layer.provide(ConfigProvider.layer(options.configProvider)))
|
||||
const context = yield* Layer.buildWithMemoMap(
|
||||
AppNodeBuilder.build(LayerNode.group([ApplicationTools.node, PermissionSaved.node])),
|
||||
withConfig(AppNodeBuilder.build(LayerNode.group([ApplicationTools.node, PermissionSaved.node]))),
|
||||
memoMap,
|
||||
scope,
|
||||
)
|
||||
|
|
@ -20,9 +36,11 @@ export const create = Effect.fn("OpenCode.create")(function* () {
|
|||
const web = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
HttpRouter.toWebHandler(
|
||||
createEmbeddedRoutes().pipe(
|
||||
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
||||
Layer.provide(HttpServer.layerServices),
|
||||
withConfig(
|
||||
createEmbeddedRoutes().pipe(
|
||||
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
||||
Layer.provide(HttpServer.layerServices),
|
||||
),
|
||||
),
|
||||
{ disableLogger: true, memoMap },
|
||||
),
|
||||
|
|
@ -47,3 +65,5 @@ export type Interface = Effect.Success<ReturnType<typeof create>>
|
|||
export class Service extends Context.Service<Service, Interface>()("@opencode-ai/sdk-next/OpenCode") {}
|
||||
|
||||
export const layer = Layer.effect(Service, create())
|
||||
|
||||
export const layerWith = (options: Options) => Layer.effect(Service, create(options))
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import { afterAll, expect, test } from "bun:test"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { Deferred, Effect, Latch, Option, Schema, Stream } from "effect"
|
||||
import { ConfigProvider, Deferred, Effect, Latch, Option, Schema, Stream } from "effect"
|
||||
import type { OpenCodeEvent } from "../src"
|
||||
|
||||
// The database layer resolves OPENCODE_DB through Effect Config, and the
|
||||
// default ConfigProvider snapshots the process environment on first use, so
|
||||
// database placement is process-wide. Point every embedded host in this file
|
||||
// at one shared temporary database before anything builds a runtime.
|
||||
const databaseDirectory = await mkdtemp(join(tmpdir(), "opencode-embedded-db-"))
|
||||
process.env.OPENCODE_DB = join(databaseDirectory, "opencode.sqlite")
|
||||
afterAll(() => rm(databaseDirectory, { recursive: true, force: true }))
|
||||
// The default ConfigProvider snapshots the process environment on first use,
|
||||
// so per-host database placement must come through the create() config seam
|
||||
// rather than env mutation.
|
||||
const databaseConfig = (directory: string) =>
|
||||
ConfigProvider.fromUnknown({ OPENCODE_DB: join(directory, "opencode.sqlite") })
|
||||
|
||||
test("embedded client uses the real router and handlers", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-"))
|
||||
|
|
@ -21,7 +19,7 @@ test("embedded client uses the real router and handlers", async () => {
|
|||
|
||||
try {
|
||||
const program = Effect.gen(function* () {
|
||||
const opencode = yield* OpenCode.create()
|
||||
const opencode = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
|
||||
yield* opencode.tools.register({
|
||||
embedded_tool: Tool.make({
|
||||
description: "Embedded test tool",
|
||||
|
|
@ -103,6 +101,8 @@ test("embedded client uses the real router and handlers", async () => {
|
|||
expect(missingMessage._tag).toBe("MessageNotFoundError")
|
||||
})
|
||||
await Effect.runPromise(Effect.scoped(program))
|
||||
// The database materializes at the path this host's ConfigProvider chose.
|
||||
expect(await Bun.file(join(directory, "opencode.sqlite")).exists()).toBe(true)
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ test("Location-owned runner events reach the ready global client", async () => {
|
|||
|
||||
try {
|
||||
const program = Effect.gen(function* () {
|
||||
const opencode = yield* OpenCode.create()
|
||||
const opencode = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
|
||||
const connected = yield* Latch.make(false)
|
||||
const prompted = yield* Deferred.make<OpenCodeEvent>()
|
||||
yield* opencode.events.subscribe().pipe(
|
||||
|
|
@ -151,8 +151,8 @@ test("independent embedded hosts do not share live notifications", async () => {
|
|||
|
||||
try {
|
||||
const program = Effect.gen(function* () {
|
||||
const first = yield* OpenCode.create()
|
||||
const second = yield* OpenCode.create()
|
||||
const first = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
|
||||
const second = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
|
||||
const firstReady = yield* Latch.make(false)
|
||||
const secondReady = yield* Latch.make(false)
|
||||
const firstEvent = yield* Latch.make(false)
|
||||
|
|
@ -197,7 +197,7 @@ test("embedded client is available as a Layer service", async () => {
|
|||
id: sessionID,
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
}).pipe(Effect.provide(OpenCode.layer), Effect.scoped),
|
||||
}).pipe(Effect.provide(OpenCode.layerWith({ configProvider: databaseConfig(directory) })), Effect.scoped),
|
||||
)
|
||||
|
||||
expect(created.id).toBe(sessionID)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue