Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
James Long
de65764bae refactor(opencode): bind instance bootstrap node 2026-06-29 16:12:38 -04:00
James Long
61383fde77 refactor(core): convert config tests to nodes 2026-06-29 14:38:32 -04:00
James Long
a10867cac0 refactor(core): convert prompt tests to nodes 2026-06-29 14:02:27 -04:00
28 changed files with 180 additions and 168 deletions

View file

@ -1,6 +1,7 @@
import { Agent } from "@/agent/agent" import { Agent } from "@/agent/agent"
import { Command } from "@/command" import { Command } from "@/command"
import { InstanceRef } from "@/effect/instance-ref" import { InstanceRef } from "@/effect/instance-ref"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store" import { InstanceStore } from "@/project/instance-store"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
@ -205,7 +206,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(Provider.defaultLayer), Layer.provide(Provider.defaultLayer),
Layer.provide(Agent.defaultLayer), Layer.provide(Agent.defaultLayer),
Layer.provide(Command.defaultLayer), Layer.provide(Command.defaultLayer),
Layer.provide(InstanceStore.defaultLayer), Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
) )
export const loaderNode = LayerNode.make({ export const loaderNode = LayerNode.make({

View file

@ -1,6 +1,7 @@
import type { AgentSideConnection, Usage } from "@agentclientprotocol/sdk" import type { AgentSideConnection, Usage } from "@agentclientprotocol/sdk"
import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@opencode-ai/sdk/v2" import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@opencode-ai/sdk/v2"
import { InstanceRef } from "@/effect/instance-ref" import { InstanceRef } from "@/effect/instance-ref"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store" import { InstanceStore } from "@/project/instance-store"
import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node" import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@ -228,7 +229,7 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe( export const defaultLayer = layer.pipe(
Layer.provide(contextLimitLoaderLayer), Layer.provide(contextLimitLoaderLayer),
Layer.provide(Provider.defaultLayer), Layer.provide(Provider.defaultLayer),
Layer.provide(InstanceStore.defaultLayer), Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
) )
export const messageLoaderNode = LayerNode.unbound(MessageLoader, Node.tags.values.global) export const messageLoaderNode = LayerNode.unbound(MessageLoader, Node.tags.values.global)

View file

@ -601,7 +601,11 @@ export const layer = Layer.effect(
}), }),
fallback: "", fallback: "",
response: "text", response: "text",
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)))) }).pipe(
Effect.provide(
LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
),
)
: "" : ""
if (sourcePatch) { if (sourcePatch) {
@ -617,7 +621,9 @@ export const layer = Layer.effect(
body: HttpBody.jsonUnsafe({ patch: sourcePatch }), body: HttpBody.jsonUnsafe({ patch: sourcePatch }),
}), }),
fallback: { applied: false }, fallback: { applied: false },
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)))) }).pipe(
Effect.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
)
} }
if (input.workspaceID === null) { if (input.workspaceID === null) {

View file

@ -38,7 +38,8 @@ import { Command } from "@/command"
import { Truncate } from "@/tool/truncate" import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry" import { ToolRegistry } from "@/tool/registry"
import { Format } from "@/format" import { Format } from "@/format"
import { InstanceLayer } from "@/project/instance-layer" import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { Project } from "@/project/project" import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs" import { Vcs } from "@/project/vcs"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
@ -51,6 +52,7 @@ import { memoMap } from "@opencode-ai/core/effect/memo-map"
import { BackgroundJob } from "@/background/job" import { BackgroundJob } from "@/background/job"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
export const AppLayer = Layer.mergeAll( export const AppLayer = Layer.mergeAll(
Npm.defaultLayer, Npm.defaultLayer,
@ -101,7 +103,7 @@ export const AppLayer = Layer.mergeAll(
SessionShare.defaultLayer, SessionShare.defaultLayer,
).pipe( ).pipe(
Layer.provideMerge(Ripgrep.defaultLayer), Layer.provideMerge(Ripgrep.defaultLayer),
Layer.provideMerge(InstanceLayer.layer), Layer.provideMerge(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
Layer.provideMerge(Observability.layer), Layer.provideMerge(Observability.layer),
) )

View file

@ -1,4 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { Format } from "../format" import { Format } from "../format"
import { LSP } from "@/lsp/lsp" import { LSP } from "@/lsp/lsp"
@ -62,7 +62,7 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
]), ]),
) )
export const node = LayerNode.make({ export const node = makeGlobalNode({
service: Service, service: Service,
layer: layer, layer: layer,
deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, ShareNext.node, Snapshot.node, Vcs.node], deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, ShareNext.node, Snapshot.node, Vcs.node],

View file

@ -1,11 +0,0 @@
import { Effect, Layer } from "effect"
import { InstanceStore } from "./instance-store"
export const layer = Layer.unwrap(
Effect.promise(async () => {
const { InstanceBootstrap } = await import("./bootstrap")
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
}),
)
export * as InstanceLayer from "./instance-layer"

View file

@ -1,4 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { serviceUse } from "@opencode-ai/core/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
@ -8,7 +9,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect" import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
import { type InstanceContext } from "./instance-context" import { type InstanceContext } from "./instance-context"
import { InstanceBootstrap } from "./bootstrap-service" import { InstanceBootstrap } from "./bootstrap-service"
import { InstanceBootstrap as InstanceBootstrapGraph } from "./bootstrap"
import * as Project from "./project" import * as Project from "./project"
export interface LoadInput { export interface LoadInput {
@ -204,10 +204,12 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer))
export const node = LayerNode.make({ export const bootstrapNode = LayerNode.unbound(InstanceBootstrap.Service, Node.tags.values.global)
export const node = makeGlobalNode({
service: Service, service: Service,
layer: layer, layer: layer,
deps: [Project.node, InstanceBootstrapGraph.node], deps: [Project.node, bootstrapNode],
}) })
export * as InstanceStore from "./instance-store" export * as InstanceStore from "./instance-store"

View file

@ -22,6 +22,7 @@ import { McpAuth } from "@/mcp/auth"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { PluginPtyEnvironment } from "@/plugin/pty-environment" import { PluginPtyEnvironment } from "@/plugin/pty-environment"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store" import { InstanceStore } from "@/project/instance-store"
import { Project } from "@/project/project" import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs" import { Vcs } from "@/project/vcs"
@ -301,7 +302,7 @@ export function createRoutes(
), ),
Layer.provide(locationServiceMapLayer), Layer.provide(locationServiceMapLayer),
Layer.provide(LayerNode.compile(app)), Layer.provide(LayerNode.compile(app, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
) )
} }

View file

@ -1,7 +1,7 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { path } from "@opencode-ai/core/effect/app-node-platform" import { path } from "@opencode-ai/core/effect/app-node-platform"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { InstanceLayer } from "@/project/instance-layer" import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store" import { InstanceStore } from "@/project/instance-store"
import { Project } from "@/project/project" import { Project } from "@/project/project"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
@ -625,7 +625,9 @@ export const appLayer = layer.pipe(
Layer.provide(NodePath.layer), Layer.provide(NodePath.layer),
) )
export const defaultLayer = appLayer.pipe(Layer.provide(InstanceLayer.layer)) export const defaultLayer = appLayer.pipe(
Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
)
export const node = LayerNode.make({ export const node = LayerNode.make({
service: Service, service: Service,

View file

@ -1,13 +1,14 @@
import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test" import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Cause, Effect, Exit, Layer, Option } from "effect" import { Cause, Effect, Exit, Layer, Option } from "effect"
import { NamedError } from "@opencode-ai/core/util/error" import { NamedError } from "@opencode-ai/core/util/error"
import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed" import { ConfigManaged } from "@/config/managed"
import { ConfigParse } from "../../src/config/parse" import { ConfigParse } from "../../src/config/parse"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { Npm } from "@opencode-ai/core/npm"
import { InstanceRef } from "../../src/effect/instance-ref" import { InstanceRef } from "../../src/effect/instance-ref"
import type { InstanceContext } from "../../src/project/instance-context" import type { InstanceContext } from "../../src/project/instance-context"
@ -41,13 +42,6 @@ import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth" import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm" import { NpmTest } from "../fake/npm"
/** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */
const infra = CrossSpawnSpawner.defaultLayer.pipe(
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const testFlock = EffectFlock.defaultLayer
const unexpectedHttp = HttpClient.make((request) => const unexpectedHttp = HttpClient.make((request) =>
Effect.die(`unexpected http request: ${request.method} ${request.url}`), Effect.die(`unexpected http request: ${request.method} ${request.url}`),
) )
@ -104,15 +98,14 @@ const configLayer = (
client?: HttpClient.HttpClient client?: HttpClient.HttpClient
} = {}, } = {},
) => ) =>
Config.layer.pipe( LayerNode.compile(
Layer.provide(testFlock), LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]),
Layer.provide(Env.defaultLayer), [
Layer.provide(options.auth ?? AuthTest.empty), [Auth.node, options.auth ?? AuthTest.empty],
Layer.provide(options.account ?? AccountTest.empty), [Account.node, options.account ?? AccountTest.empty],
Layer.provideMerge(infra), [Npm.node, NpmTest.noop],
Layer.provide(NpmTest.noop), [httpClient, Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)],
Layer.provide(Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)), ],
Layer.provideMerge(FSUtil.defaultLayer),
) )
const layer = configLayer() const layer = configLayer()
@ -171,7 +164,7 @@ const withInstanceDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =
Effect.provideService(TestInstance, { directory: dir }), Effect.provideService(TestInstance, { directory: dir }),
provideInstanceEffect(dir), provideInstanceEffect(dir),
Effect.provide(testInstanceStoreLayer), Effect.provide(testInstanceStoreLayer),
Effect.provide(CrossSpawnSpawner.defaultLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)),
) )
const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) => const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
@ -325,7 +318,7 @@ it.effect("creates global jsonc config with schema when no global configs exist"
const content = yield* FSUtil.use.readFileString(path.join(dir, "opencode.jsonc")) const content = yield* FSUtil.use.readFileString(path.join(dir, "opencode.jsonc"))
expect(content).toContain('"$schema": "https://opencode.ai/config.json"') expect(content).toContain('"$schema": "https://opencode.ai/config.json"')
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)), }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
), ),
) )
@ -340,7 +333,7 @@ it.effect("does not create global config when OPENCODE_CONFIG_DIR is set", () =>
yield* Config.use.get().pipe(provideInstanceEffect(dir)) yield* Config.use.get().pipe(provideInstanceEffect(dir))
expect(yield* FSUtil.use.existsSafe(path.join(dir, "opencode.jsonc"))).toBe(false) expect(yield* FSUtil.use.existsSafe(path.join(dir, "opencode.jsonc"))).toBe(false)
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)), }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
), ),
) )
}), }),
@ -930,7 +923,7 @@ it.effect("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR
yield* Effect.addFinalizer(() => FSUtil.use.chmod(readonly, 0o755).pipe(Effect.ignore)) yield* Effect.addFinalizer(() => FSUtil.use.chmod(readonly, 0o755).pipe(Effect.ignore))
yield* withProcessEnv("OPENCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir))) yield* withProcessEnv("OPENCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)), }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
) )
it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () => it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
@ -948,7 +941,7 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
) )
expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json") expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)), }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
) )
// Note: deduplication and serialization of npm installs is now handled by the // Note: deduplication and serialization of npm installs is now handled by the
@ -1533,16 +1526,12 @@ test("remote well-known config can use FetchHttpClient layer", async () => {
Effect.scoped, Effect.scoped,
Effect.provide( Effect.provide(
Layer.mergeAll( Layer.mergeAll(
Config.layer.pipe( LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
Layer.provide(testFlock), [Auth.node, wellKnownAuth(server.url.origin)],
Layer.provide(FSUtil.defaultLayer), [Account.node, AccountTest.empty],
Layer.provide(Env.defaultLayer), [Npm.node, NpmTest.noop],
Layer.provide(wellKnownAuth(server.url.origin)), [httpClient, FetchHttpClient.layer],
Layer.provide(AccountTest.empty), ]),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
),
testInstanceStoreLayer, testInstanceStoreLayer,
), ),
), ),

View file

@ -8,6 +8,7 @@ import { Effect, Exit, Fiber, Layer, Schema } from "effect"
import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { GlobalBus, type GlobalEvent } from "@/bus/global" import { GlobalBus, type GlobalEvent } from "@/bus/global"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectV2 } from "@opencode-ai/core/project"
@ -56,7 +57,7 @@ const workspaceLayer = (experimentalWorkspaces: boolean) =>
Layer.provide(FSUtil.defaultLayer), Layer.provide(FSUtil.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })), Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
Layer.provide(Ripgrep.defaultLayer), Layer.provide(Ripgrep.defaultLayer),
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))), Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
) )
const testServerLayer = Layer.mergeAll( const testServerLayer = Layer.mergeAll(

View file

@ -9,6 +9,7 @@ import type * as Scope from "effect/Scope"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import type { Config } from "@/config/config" import type { Config } from "@/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceRef } from "../../src/effect/instance-ref" import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceBootstrap } from "../../src/project/bootstrap-service" import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import type { InstanceContext } from "../../src/project/instance-context" import type { InstanceContext } from "../../src/project/instance-context"
@ -17,7 +18,7 @@ import { InstanceStore } from "../../src/project/instance-store"
import { TestLLMServer } from "../lib/llm-server" import { TestLLMServer } from "../lib/llm-server"
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
export const testInstanceStoreLayer = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)) export const testInstanceStoreLayer = LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]])
export async function provideTestInstance<R>(input: { export async function provideTestInstance<R>(input: {
directory: string directory: string

View file

@ -1,5 +1,6 @@
import { FetchHttpClient } from "effect/unstable/http" import { FetchHttpClient } from "effect/unstable/http"
import { Layer } from "effect" import { Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { Auth } from "../../src/auth" import { Auth } from "../../src/auth"
@ -25,6 +26,5 @@ export const workspaceLayerWithRuntimeFlags = (overrides: Partial<RuntimeFlags.I
Layer.provide(FetchHttpClient.layer), Layer.provide(FetchHttpClient.layer),
Layer.provide(FSUtil.defaultLayer), Layer.provide(FSUtil.defaultLayer),
Layer.provide(RuntimeFlags.layer(overrides)), Layer.provide(RuntimeFlags.layer(overrides)),
Layer.provide(InstanceStore.defaultLayer), Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
Layer.provide(InstanceBootstrap.defaultLayer),
) )

View file

@ -2,6 +2,7 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import os from "os" import os from "os"
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect" import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2Bridge } from "../../src/event-v2-bridge" import { EventV2Bridge } from "../../src/event-v2-bridge"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
@ -18,7 +19,7 @@ const env = Layer.mergeAll(
Permission.layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(events)), Permission.layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(events)),
events, events,
CrossSpawnSpawner.defaultLayer, CrossSpawnSpawner.defaultLayer,
InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)), LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]]),
) )
const it = testEffect(env) const it = testEffect(env)

View file

@ -4,6 +4,7 @@ import { FetchHttpClient } from "effect/unstable/http"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Ripgrep } from "@opencode-ai/core/ripgrep" import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import path from "path" import path from "path"
@ -53,7 +54,7 @@ const workspaceLayer = Workspace.layer.pipe(
Layer.provide(Database.defaultLayer), Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FSUtil.defaultLayer), Layer.provide(FSUtil.defaultLayer),
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))), Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrapLayer]])),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: true })), Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: true })),
) )
const it = testEffect( const it = testEffect(

View file

@ -4,14 +4,19 @@ import path from "node:path"
import { pathToFileURL } from "node:url" import { pathToFileURL } from "node:url"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Cause, Effect, Exit, Fiber, Layer } from "effect" import { Cause, Effect, Exit, Fiber } from "effect"
import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap" import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store" import { InstanceStore } from "../../src/project/instance-store"
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture" import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { waitGlobalBusEvent } from "../server/global-bus" import { waitGlobalBusEvent } from "../server/global-bus"
const it = testEffect(LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]))) const it = testEffect(
LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
]),
)
// InstanceBootstrap must run before any code touches the instance — // InstanceBootstrap must run before any code touches the instance —
// originally tracked by PRs #25389 and #25449, now a permanent // originally tracked by PRs #25389 and #25449, now a permanent

View file

@ -17,7 +17,7 @@ const noopBootstrap = Layer.succeed(
const it = testEffect( const it = testEffect(
LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [ LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
[InstanceBootstrap.node, noopBootstrap], [InstanceStore.bootstrapNode, noopBootstrap],
]), ]),
) )

View file

@ -4,11 +4,13 @@ import * as fs from "fs/promises"
import path from "path" import path from "path"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect" import { Effect } from "effect"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { Worktree } from "../../src/worktree" import { Worktree } from "../../src/worktree"
import { TestInstance } from "../fixture/fixture" import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const it = testEffect(LayerNode.compile(Worktree.node)) const it = testEffect(LayerNode.compile(Worktree.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]))
const wintest = process.platform === "win32" ? it.instance : it.instance.skip const wintest = process.platform === "win32" ? it.instance : it.instance.skip
describe("Worktree.remove", () => { describe("Worktree.remove", () => {

View file

@ -5,11 +5,17 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Cause, Deferred, Effect, Exit, Fiber } from "effect" import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global" import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { Git } from "../../src/git" import { Git } from "../../src/git"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { Worktree } from "../../src/worktree" import { Worktree } from "../../src/worktree"
import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture" import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const it = testEffect(LayerNode.compile(LayerNode.group([Worktree.node, FSUtil.node, Git.node]))) const it = testEffect(
LayerNode.compile(LayerNode.group([Worktree.node, FSUtil.node, Git.node]), [
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
]),
)
const wintest = process.platform !== "win32" ? it.instance : it.instance.skip const wintest = process.platform !== "win32" ? it.instance : it.instance.skip
function normalize(input: string) { function normalize(input: string) {

View file

@ -17,7 +17,8 @@ import { Provider } from "@/provider/provider"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import { InstanceLayer } from "@/project/instance-layer" import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
@ -1760,8 +1761,9 @@ it.instance(
// Tests that need plugin file setup or multi-instance flows fall back to a // Tests that need plugin file setup or multi-instance flows fall back to a
// scoped tmpdir + provideInstance pattern via it.effect. // scoped tmpdir + provideInstance pattern via it.effect.
const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])
const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) => const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
eff.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)) eff.pipe(Effect.provide(instanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
it.effect("plugin config providers persist after instance dispose", () => it.effect("plugin config providers persist after instance dispose", () =>
Effect.gen(function* () { Effect.gen(function* () {
@ -1864,7 +1866,7 @@ it.effect("opencode loader keeps paid models when config apiKey is present", ()
Provider.use Provider.use
.list() .list()
.pipe(provideInstanceEffect(directory)) .pipe(provideInstanceEffect(directory))
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)) .pipe(Effect.provide(instanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
const none = paid(yield* listIn(noneDir)) const none = paid(yield* listIn(noneDir))
const keyedCount = paid(yield* listIn(keyedDir)) const keyedCount = paid(yield* listIn(keyedDir))
@ -1883,7 +1885,7 @@ it.effect("opencode loader keeps paid models when auth exists", () =>
Provider.use Provider.use
.list() .list()
.pipe(provideInstanceEffect(directory)) .pipe(provideInstanceEffect(directory))
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)) .pipe(Effect.provide(instanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
const none = paid(yield* listIn(noneDir)) const none = paid(yield* listIn(noneDir))

View file

@ -1,6 +1,7 @@
import { NodeHttpServer, NodeServices } from "@effect/platform-node" import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Fiber, Layer, Schema } from "effect" import { Effect, Fiber, Layer, Schema } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http" import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"
import * as Socket from "effect/unstable/socket/Socket" import * as Socket from "effect/unstable/socket/Socket"
@ -12,7 +13,8 @@ import { Ripgrep } from "@opencode-ai/core/ripgrep"
import type { WorkspaceAdapter } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace" import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref" import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
import { InstanceLayer } from "../../src/project/instance-layer" import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { Project } from "../../src/project/project" import { Project } from "../../src/project/project"
import { Session } from "../../src/session/session" import { Session } from "../../src/session/session"
import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle" import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"
@ -51,7 +53,7 @@ const it = testEffect(
testStateLayer, testStateLayer,
NodeHttpServer.layerTest, NodeHttpServer.layerTest,
NodeServices.layer, NodeServices.layer,
InstanceLayer.layer, LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
Project.defaultLayer, Project.defaultLayer,
workspaceLayer, workspaceLayer,
).pipe(Layer.provide(Ripgrep.defaultLayer)), ).pipe(Layer.provide(Ripgrep.defaultLayer)),

View file

@ -10,6 +10,7 @@
import { NodeHttpServer, NodeServices } from "@effect/platform-node" import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Deferred, Effect, Layer, Schema, Scope } from "effect" import { Deferred, Effect, Layer, Schema, Scope } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http" import { HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http"
import * as Socket from "effect/unstable/socket/Socket" import * as Socket from "effect/unstable/socket/Socket"
@ -20,7 +21,8 @@ import { Ripgrep } from "@opencode-ai/core/ripgrep"
import type { WorkspaceAdapter } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace" import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref" import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
import { InstanceLayer } from "../../src/project/instance-layer" import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { Project } from "../../src/project/project" import { Project } from "../../src/project/project"
import { Session } from "../../src/session/session" import { Session } from "../../src/session/session"
import { import {
@ -56,7 +58,7 @@ const it = testEffect(
testStateLayer, testStateLayer,
NodeHttpServer.layerTest, NodeHttpServer.layerTest,
NodeServices.layer, NodeServices.layer,
InstanceLayer.layer, LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
Project.defaultLayer, Project.defaultLayer,
workspaceLayer, workspaceLayer,
).pipe(Layer.provide(Ripgrep.defaultLayer)), ).pipe(Layer.provide(Ripgrep.defaultLayer)),

View file

@ -7,6 +7,7 @@ import { HttpServer } from "effect/unstable/http"
import { ChildProcessSpawner } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { createOpencodeClient } from "@opencode-ai/sdk/v2" import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { validateSession } from "../../src/cli/tui/validate-session" import { validateSession } from "../../src/cli/tui/validate-session"
@ -34,7 +35,7 @@ const it = testEffect(
Layer.mergeAll( Layer.mergeAll(
FSUtil.defaultLayer, FSUtil.defaultLayer,
CrossSpawnSpawner.defaultLayer, CrossSpawnSpawner.defaultLayer,
InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)), LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]]),
Database.defaultLayer, Database.defaultLayer,
httpApiLayer, httpApiLayer,
), ),

View file

@ -9,6 +9,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse, HttpRouter, HttpServ
import { layerWebSocketConstructorGlobal } from "effect/unstable/socket/Socket" import { layerWebSocketConstructorGlobal } from "effect/unstable/socket/Socket"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Ripgrep } from "@opencode-ai/core/ripgrep" import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { registerAdapter } from "../../src/control-plane/adapters" import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
@ -38,14 +39,14 @@ import { pollWithTimeout, testEffect } from "../lib/effect"
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
const workspaceLayer = Workspace.defaultLayer.pipe( const workspaceLayer = Workspace.defaultLayer.pipe(
Layer.provide(InstanceStore.defaultLayer), Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
Layer.provide(InstanceBootstrap.defaultLayer),
) )
const instanceStoreLayer = InstanceStore.defaultLayer.pipe( const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [
Layer.provide( [
InstanceStore.bootstrapNode,
Layer.succeed(InstanceBootstrapService.Service, InstanceBootstrapService.Service.of({ run: Effect.void })), Layer.succeed(InstanceBootstrapService.Service, InstanceBootstrapService.Service.of({ run: Effect.void })),
), ],
) ])
const servedRoutes: Layer.Layer<never, Config.ConfigError, HttpServer.HttpServer> = HttpRouter.serve( const servedRoutes: Layer.Layer<never, Config.ConfigError, HttpServer.HttpServer> = HttpRouter.serve(
HttpApiApp.routes, HttpApiApp.routes,
{ {

View file

@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises"
import path from "node:path" import path from "node:path"
import { Effect, Layer, Stream } from "effect" import { Effect, Layer, Stream } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { registerAdapter } from "../../src/control-plane/adapters" import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import type { WorkspaceAdapter } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
@ -24,15 +25,14 @@ import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
const workspaceLayer = Workspace.defaultLayer.pipe( const workspaceLayer = Workspace.defaultLayer.pipe(
Layer.provide(InstanceStore.defaultLayer), Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
Layer.provide(InstanceBootstrap.defaultLayer),
) )
const it = testEffect( const it = testEffect(
Layer.mergeAll( Layer.mergeAll(
Project.defaultLayer, Project.defaultLayer,
Session.defaultLayer, Session.defaultLayer,
workspaceLayer, workspaceLayer,
InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)), LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
Database.defaultLayer, Database.defaultLayer,
httpApiLayer, httpApiLayer,
).pipe(Layer.provide(Ripgrep.defaultLayer)), ).pipe(Layer.provide(Ripgrep.defaultLayer)),

View file

@ -21,7 +21,7 @@ afterEach(async () => {
}) })
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const testInstanceStore = LayerNode.compile(InstanceStore.node, [[InstanceBootstrap.node, noopBootstrap]]) const testInstanceStore = LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]])
const it = testEffect( const it = testEffect(
Layer.mergeAll( Layer.mergeAll(
LayerNode.compile(LayerNode.group([FSUtil.node, Database.node, Snapshot.node])), LayerNode.compile(LayerNode.group([FSUtil.node, Database.node, Snapshot.node])),

View file

@ -20,7 +20,7 @@ afterEach(async () => {
}) })
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const testInstanceStore = LayerNode.compile(InstanceStore.node, [[InstanceBootstrap.node, noopBootstrap]]) const testInstanceStore = LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]])
const it = testEffect( const it = testEffect(
Layer.mergeAll(LayerNode.compile(LayerNode.group([FSUtil.node, Snapshot.node])), testInstanceStore, httpApiLayer), Layer.mergeAll(LayerNode.compile(LayerNode.group([FSUtil.node, Snapshot.node])), testInstanceStore, httpApiLayer),

View file

@ -1,10 +1,10 @@
import { NodeFileSystem } from "@effect/platform-node"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session" import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { FetchHttpClient } from "effect/unstable/http"
import { expect } from "bun:test" import { expect } from "bun:test"
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect" import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
import path from "path" import path from "path"
@ -156,10 +156,6 @@ const lsp = Layer.succeed(
}), }),
) )
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
const run = SessionRunState.layer.pipe(Layer.provide(status))
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
const processorCreateStarted: Array<() => void> = [] const processorCreateStarted: Array<() => void> = []
const blockingProcessor = Layer.succeed( const blockingProcessor = Layer.succeed(
SessionProcessor.Service, SessionProcessor.Service,
@ -168,80 +164,75 @@ const blockingProcessor = Layer.succeed(
}), }),
) )
const runtimeFlags = RuntimeFlags.layer({ experimentalEventSystem: true })
const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
const promptRoot = LayerNode.group([
SessionPrompt.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
LLM.node,
Env.node,
AgentSvc.node,
Command.node,
Permission.node,
Plugin.node,
Config.node,
ProviderSvc.node,
LSP.node,
MCP.node,
FSUtil.node,
BackgroundJob.node,
SessionStatus.node,
SessionRunState.node,
Database.node,
EventV2Bridge.node,
Question.node,
Todo.node,
ToolRegistry.node,
Skill.node,
Git.node,
Ripgrep.node,
Format.node,
Truncate.node,
SessionProcessor.node,
Image.node,
SessionCompaction.node,
SessionRevert.node,
Instruction.node,
SystemPrompt.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
])
function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
const deps = Layer.mergeAll( const replacements = [
Session.defaultLayer, [SessionSummary.node, summary],
Snapshot.defaultLayer, [LSP.node, lsp],
LLM.defaultLayer, [MCP.node, makeMcp(input?.mcpInstructions)],
Env.defaultLayer, [RuntimeFlags.node, runtimeFlags],
AgentSvc.defaultLayer, ] as const
Command.defaultLayer, if (input?.processor === "blocking") {
Permission.defaultLayer, return LayerNode.compile(promptRoot, [...replacements, [SessionProcessor.node, blockingProcessor]])
Plugin.defaultLayer, }
Config.defaultLayer, return LayerNode.compile(promptRoot, replacements)
ProviderSvc.defaultLayer,
lsp,
makeMcp(input?.mcpInstructions),
FSUtil.defaultLayer,
BackgroundJob.defaultLayer,
status,
Database.defaultLayer,
EventV2Bridge.defaultLayer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = ToolRegistry.layer.pipe(
Layer.provide(Skill.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Format.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
)
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
const proc =
input?.processor === "blocking"
? blockingProcessor
: SessionProcessor.layer.pipe(
Layer.provide(summary),
Layer.provide(Image.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(deps),
)
const compact = SessionCompaction.layer.pipe(
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(proc),
Layer.provideMerge(deps),
)
return SessionPrompt.layer.pipe(
Layer.provide(SessionRevert.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(summary),
Layer.provideMerge(run),
Layer.provideMerge(compact),
Layer.provideMerge(proc),
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer),
Layer.provide(
SystemPrompt.layer.pipe(
Layer.provide(Skill.defaultLayer),
Layer.provide(locationServiceMapLayer),
Layer.provide(deps),
),
),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(deps),
Layer.provide(summary),
)
} }
function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
return Layer.mergeAll(TestLLMServer.layer, makePrompt(input)) const root = LayerNode.group([promptRoot, testLLMServerNode])
const replacements = [
[SessionSummary.node, summary],
[LSP.node, lsp],
[MCP.node, makeMcp(input?.mcpInstructions)],
[RuntimeFlags.node, runtimeFlags],
] as const
if (input?.processor === "blocking") {
return LayerNode.compile(root, [...replacements, [SessionProcessor.node, blockingProcessor]])
}
return LayerNode.compile(root, replacements)
} }
function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
@ -697,9 +688,12 @@ noLLMServer.instance.skip(
}) })
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe( const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
Effect.provide(SessionV2.defaultLayer), Effect.provide(
Effect.provide(SessionExecution.noopLayer), LayerNode.compile(SessionV2.node, [
Effect.provide(locationServiceMapLayer), [SessionExecution.node, SessionExecution.noopLayer],
[LocationServiceMap.node, locationServiceMapLayer],
]),
),
) )
const { db } = yield* Database.Service const { db } = yield* Database.Service
const row = yield* db const row = yield* db