From a10867cac0c0c8786dd9a2552119ea2d8f485ff8 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 29 Jun 2026 14:02:27 -0400 Subject: [PATCH 1/3] refactor(core): convert prompt tests to nodes --- packages/opencode/test/session/prompt.test.ts | 152 +++++++++--------- 1 file changed, 73 insertions(+), 79 deletions(-) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index bb98a867fa..491ad06aaf 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1,10 +1,10 @@ -import { NodeFileSystem } from "@effect/platform-node" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { SessionV1 } from "@opencode-ai/core/v1/session" 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 { EventV2Bridge } from "@/event-v2-bridge" -import { FetchHttpClient } from "effect/unstable/http" import { expect } from "bun:test" import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect" 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 blockingProcessor = Layer.succeed( 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" }) { - const deps = Layer.mergeAll( - Session.defaultLayer, - Snapshot.defaultLayer, - LLM.defaultLayer, - Env.defaultLayer, - AgentSvc.defaultLayer, - Command.defaultLayer, - Permission.defaultLayer, - Plugin.defaultLayer, - Config.defaultLayer, - 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), - ) + 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(promptRoot, [...replacements, [SessionProcessor.node, blockingProcessor]]) + } + return LayerNode.compile(promptRoot, replacements) } 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" }) { @@ -697,9 +688,12 @@ noLLMServer.instance.skip( }) const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe( - Effect.provide(SessionV2.defaultLayer), - Effect.provide(SessionExecution.noopLayer), - Effect.provide(locationServiceMapLayer), + Effect.provide( + LayerNode.compile(SessionV2.node, [ + [SessionExecution.node, SessionExecution.noopLayer], + [LocationServiceMap.node, locationServiceMapLayer], + ]), + ), ) const { db } = yield* Database.Service const row = yield* db From 61383fde77a121f26a864c55f61f58cf37cdf157 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 29 Jun 2026 14:38:32 -0400 Subject: [PATCH 2/3] refactor(core): convert config tests to nodes --- packages/opencode/test/config/config.test.ts | 55 ++++++++------------ 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 02ace53668..08e6264be9 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1,13 +1,14 @@ import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test" 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 { NamedError } from "@opencode-ai/core/util/error" import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" -import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config } from "@/config/config" import { ConfigManaged } from "@/config/managed" 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 type { InstanceContext } from "../../src/project/instance-context" @@ -41,13 +42,6 @@ import { AccountTest } from "../fake/account" import { AuthTest } from "../fake/auth" 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) => Effect.die(`unexpected http request: ${request.method} ${request.url}`), ) @@ -104,15 +98,14 @@ const configLayer = ( client?: HttpClient.HttpClient } = {}, ) => - Config.layer.pipe( - Layer.provide(testFlock), - Layer.provide(Env.defaultLayer), - Layer.provide(options.auth ?? AuthTest.empty), - Layer.provide(options.account ?? AccountTest.empty), - Layer.provideMerge(infra), - Layer.provide(NpmTest.noop), - Layer.provide(Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)), - Layer.provideMerge(FSUtil.defaultLayer), + LayerNode.compile( + LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), + [ + [Auth.node, options.auth ?? AuthTest.empty], + [Account.node, options.account ?? AccountTest.empty], + [Npm.node, NpmTest.noop], + [httpClient, Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)], + ], ) const layer = configLayer() @@ -171,7 +164,7 @@ const withInstanceDir = (dir: string, effect: Effect.Effect) = Effect.provideService(TestInstance, { directory: dir }), provideInstanceEffect(dir), Effect.provide(testInstanceStoreLayer), - Effect.provide(CrossSpawnSpawner.defaultLayer), + Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)), ) const withGlobalConfigDir = (dir: string, effect: Effect.Effect) => @@ -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")) 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)) 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* 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", () => @@ -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") - }).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 @@ -1533,16 +1526,12 @@ test("remote well-known config can use FetchHttpClient layer", async () => { Effect.scoped, Effect.provide( Layer.mergeAll( - Config.layer.pipe( - Layer.provide(testFlock), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(wellKnownAuth(server.url.origin)), - Layer.provide(AccountTest.empty), - Layer.provideMerge(infra), - Layer.provide(NpmTest.noop), - Layer.provide(FetchHttpClient.layer), - ), + LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [ + [Auth.node, wellKnownAuth(server.url.origin)], + [Account.node, AccountTest.empty], + [Npm.node, NpmTest.noop], + [httpClient, FetchHttpClient.layer], + ]), testInstanceStoreLayer, ), ), From de65764baedfd35edf6454331e7ca823b3b17266 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 29 Jun 2026 16:12:38 -0400 Subject: [PATCH 3/3] refactor(opencode): bind instance bootstrap node --- packages/opencode/src/acp/directory.ts | 3 ++- packages/opencode/src/acp/usage.ts | 3 ++- packages/opencode/src/control-plane/workspace.ts | 10 ++++++++-- packages/opencode/src/effect/app-runtime.ts | 6 ++++-- packages/opencode/src/project/bootstrap.ts | 4 ++-- packages/opencode/src/project/instance-layer.ts | 11 ----------- packages/opencode/src/project/instance-store.ts | 8 +++++--- .../src/server/routes/instance/httpapi/server.ts | 3 ++- packages/opencode/src/worktree/index.ts | 6 ++++-- .../opencode/test/control-plane/workspace.test.ts | 3 ++- packages/opencode/test/fixture/fixture.ts | 3 ++- packages/opencode/test/fixture/workspace.ts | 4 ++-- packages/opencode/test/permission/next.test.ts | 3 ++- .../opencode/test/plugin/workspace-adapter.test.ts | 3 ++- .../test/project/instance-bootstrap.test.ts | 9 +++++++-- packages/opencode/test/project/instance.test.ts | 2 +- .../opencode/test/project/worktree-remove.test.ts | 4 +++- packages/opencode/test/project/worktree.test.ts | 8 +++++++- packages/opencode/test/provider/provider.test.ts | 10 ++++++---- .../test/server/httpapi-instance-context.test.ts | 6 ++++-- .../test/server/httpapi-promptasync-context.test.ts | 6 ++++-- packages/opencode/test/server/httpapi-sdk.test.ts | 3 ++- .../opencode/test/server/httpapi-session.test.ts | 13 +++++++------ .../opencode/test/server/httpapi-workspace.test.ts | 6 +++--- packages/opencode/test/server/project-copy.test.ts | 2 +- .../opencode/test/server/project-init-git.test.ts | 2 +- 26 files changed, 85 insertions(+), 56 deletions(-) delete mode 100644 packages/opencode/src/project/instance-layer.ts diff --git a/packages/opencode/src/acp/directory.ts b/packages/opencode/src/acp/directory.ts index 282020aa81..ed6270debd 100644 --- a/packages/opencode/src/acp/directory.ts +++ b/packages/opencode/src/acp/directory.ts @@ -1,6 +1,7 @@ import { Agent } from "@/agent/agent" import { Command } from "@/command" import { InstanceRef } from "@/effect/instance-ref" +import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceStore } from "@/project/instance-store" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { ProviderV2 } from "@opencode-ai/core/provider" @@ -205,7 +206,7 @@ export const defaultLayer = layer.pipe( Layer.provide(Provider.defaultLayer), Layer.provide(Agent.defaultLayer), Layer.provide(Command.defaultLayer), - Layer.provide(InstanceStore.defaultLayer), + Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])), ) export const loaderNode = LayerNode.make({ diff --git a/packages/opencode/src/acp/usage.ts b/packages/opencode/src/acp/usage.ts index f717d142cf..32b46e8b91 100644 --- a/packages/opencode/src/acp/usage.ts +++ b/packages/opencode/src/acp/usage.ts @@ -1,6 +1,7 @@ import type { AgentSideConnection, Usage } from "@agentclientprotocol/sdk" import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@opencode-ai/sdk/v2" import { InstanceRef } from "@/effect/instance-ref" +import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceStore } from "@/project/instance-store" import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node" @@ -228,7 +229,7 @@ export const layer = Layer.effect( export const defaultLayer = layer.pipe( Layer.provide(contextLimitLoaderLayer), 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) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 9fdfba01c7..51d808d29e 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -601,7 +601,11 @@ export const layer = Layer.effect( }), fallback: "", 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) { @@ -617,7 +621,9 @@ export const layer = Layer.effect( body: HttpBody.jsonUnsafe({ patch: sourcePatch }), }), 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) { diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 30dbb4c880..a31ff70ea1 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -38,7 +38,8 @@ import { Command } from "@/command" import { Truncate } from "@/tool/truncate" import { ToolRegistry } from "@/tool/registry" 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 { Vcs } from "@/project/vcs" 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 { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" export const AppLayer = Layer.mergeAll( Npm.defaultLayer, @@ -101,7 +103,7 @@ export const AppLayer = Layer.mergeAll( SessionShare.defaultLayer, ).pipe( Layer.provideMerge(Ripgrep.defaultLayer), - Layer.provideMerge(InstanceLayer.layer), + Layer.provideMerge(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])), Layer.provideMerge(Observability.layer), ) diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 0cabf446a5..2e32b4b11c 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -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 { Format } from "../format" import { LSP } from "@/lsp/lsp" @@ -62,7 +62,7 @@ export const defaultLayer: Layer.Layer = layer.pipe( ]), ) -export const node = LayerNode.make({ +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, ShareNext.node, Snapshot.node, Vcs.node], diff --git a/packages/opencode/src/project/instance-layer.ts b/packages/opencode/src/project/instance-layer.ts deleted file mode 100644 index a7e2bfcb7b..0000000000 --- a/packages/opencode/src/project/instance-layer.ts +++ /dev/null @@ -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" diff --git a/packages/opencode/src/project/instance-store.ts b/packages/opencode/src/project/instance-store.ts index 6877fe5681..c581c0a9f2 100644 --- a/packages/opencode/src/project/instance-store.ts +++ b/packages/opencode/src/project/instance-store.ts @@ -1,4 +1,5 @@ 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 { serviceUse } from "@opencode-ai/core/effect/service-use" 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 { type InstanceContext } from "./instance-context" import { InstanceBootstrap } from "./bootstrap-service" -import { InstanceBootstrap as InstanceBootstrapGraph } from "./bootstrap" import * as Project from "./project" export interface LoadInput { @@ -204,10 +204,12 @@ export const layer: Layer.Layer Layer.provide(FSUtil.defaultLayer), Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })), 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( diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index f9898ede0d..22e7f9c676 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -9,6 +9,7 @@ import type * as Scope from "effect/Scope" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import type { Config } from "@/config/config" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { InstanceRef } from "../../src/effect/instance-ref" import { InstanceBootstrap } from "../../src/project/bootstrap-service" 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" 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(input: { directory: string diff --git a/packages/opencode/test/fixture/workspace.ts b/packages/opencode/test/fixture/workspace.ts index 46335d3361..11fa48d5d5 100644 --- a/packages/opencode/test/fixture/workspace.ts +++ b/packages/opencode/test/fixture/workspace.ts @@ -1,5 +1,6 @@ import { FetchHttpClient } from "effect/unstable/http" import { Layer } from "effect" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Database } from "@opencode-ai/core/database/database" import { FSUtil } from "@opencode-ai/core/fs-util" import { Auth } from "../../src/auth" @@ -25,6 +26,5 @@ export const workspaceLayerWithRuntimeFlags = (overrides: Partial { diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index f0a72c4b48..e2a7e74723 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -5,11 +5,17 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { Cause, Deferred, Effect, Exit, Fiber } from "effect" import { GlobalBus, type GlobalEvent } from "../../src/bus/global" import { Git } from "../../src/git" +import { InstanceBootstrap } from "../../src/project/bootstrap" +import { InstanceStore } from "../../src/project/instance-store" import { Worktree } from "../../src/worktree" import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture" 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 function normalize(input: string) { diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 3fc948acdd..3fd354eea2 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -17,7 +17,8 @@ import { Provider } from "@/provider/provider" import { RuntimeFlags } from "@/effect/runtime-flags" 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 { ProviderV2 } from "@opencode-ai/core/provider" 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 // scoped tmpdir + provideInstance pattern via it.effect. +const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]) const provideMultiInstance = (eff: Effect.Effect) => - 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", () => Effect.gen(function* () { @@ -1864,7 +1866,7 @@ it.effect("opencode loader keeps paid models when config apiKey is present", () Provider.use .list() .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 keyedCount = paid(yield* listIn(keyedDir)) @@ -1883,7 +1885,7 @@ it.effect("opencode loader keeps paid models when auth exists", () => Provider.use .list() .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)) diff --git a/packages/opencode/test/server/httpapi-instance-context.test.ts b/packages/opencode/test/server/httpapi-instance-context.test.ts index d2977b147f..07c722ff43 100644 --- a/packages/opencode/test/server/httpapi-instance-context.test.ts +++ b/packages/opencode/test/server/httpapi-instance-context.test.ts @@ -1,6 +1,7 @@ import { NodeHttpServer, NodeServices } from "@effect/platform-node" import { describe, expect } from "bun:test" 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 { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" 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 { Workspace } from "../../src/control-plane/workspace" 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 { Session } from "../../src/session/session" import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle" @@ -51,7 +53,7 @@ const it = testEffect( testStateLayer, NodeHttpServer.layerTest, NodeServices.layer, - InstanceLayer.layer, + LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]), Project.defaultLayer, workspaceLayer, ).pipe(Layer.provide(Ripgrep.defaultLayer)), diff --git a/packages/opencode/test/server/httpapi-promptasync-context.test.ts b/packages/opencode/test/server/httpapi-promptasync-context.test.ts index eb86407716..8775cd1f70 100644 --- a/packages/opencode/test/server/httpapi-promptasync-context.test.ts +++ b/packages/opencode/test/server/httpapi-promptasync-context.test.ts @@ -10,6 +10,7 @@ import { NodeHttpServer, NodeServices } from "@effect/platform-node" import { describe, expect } from "bun:test" import { Deferred, Effect, Layer, Schema, Scope } from "effect" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import * as Stream from "effect/Stream" import { HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http" 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 { Workspace } from "../../src/control-plane/workspace" 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 { Session } from "../../src/session/session" import { @@ -56,7 +58,7 @@ const it = testEffect( testStateLayer, NodeHttpServer.layerTest, NodeServices.layer, - InstanceLayer.layer, + LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]), Project.defaultLayer, workspaceLayer, ).pipe(Layer.provide(Ripgrep.defaultLayer)), diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index 63cc3edb2c..79fdd41e6d 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -7,6 +7,7 @@ import { HttpServer } from "effect/unstable/http" import { ChildProcessSpawner } from "effect/unstable/process" import { FSUtil } from "@opencode-ai/core/fs-util" 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 { createOpencodeClient } from "@opencode-ai/sdk/v2" import { validateSession } from "../../src/cli/tui/validate-session" @@ -34,7 +35,7 @@ const it = testEffect( Layer.mergeAll( FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer, - InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)), + LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]]), Database.defaultLayer, httpApiLayer, ), diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 828ed3373a..6515872fd8 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -9,6 +9,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse, HttpRouter, HttpServ import { layerWebSocketConstructorGlobal } from "effect/unstable/socket/Socket" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" 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 { registerAdapter } from "../../src/control-plane/adapters" 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 workspaceLayer = Workspace.defaultLayer.pipe( - Layer.provide(InstanceStore.defaultLayer), - Layer.provide(InstanceBootstrap.defaultLayer), + Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])), ) -const instanceStoreLayer = InstanceStore.defaultLayer.pipe( - Layer.provide( +const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [ + [ + InstanceStore.bootstrapNode, Layer.succeed(InstanceBootstrapService.Service, InstanceBootstrapService.Service.of({ run: Effect.void })), - ), -) + ], +]) const servedRoutes: Layer.Layer = HttpRouter.serve( HttpApiApp.routes, { diff --git a/packages/opencode/test/server/httpapi-workspace.test.ts b/packages/opencode/test/server/httpapi-workspace.test.ts index 6282572b10..4a19f6b75d 100644 --- a/packages/opencode/test/server/httpapi-workspace.test.ts +++ b/packages/opencode/test/server/httpapi-workspace.test.ts @@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises" import path from "node:path" import { Effect, Layer, Stream } from "effect" 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 { WorkspaceV2 } from "@opencode-ai/core/workspace" 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 workspaceLayer = Workspace.defaultLayer.pipe( - Layer.provide(InstanceStore.defaultLayer), - Layer.provide(InstanceBootstrap.defaultLayer), + Layer.provide(LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])), ) const it = testEffect( Layer.mergeAll( Project.defaultLayer, Session.defaultLayer, workspaceLayer, - InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)), + LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]), Database.defaultLayer, httpApiLayer, ).pipe(Layer.provide(Ripgrep.defaultLayer)), diff --git a/packages/opencode/test/server/project-copy.test.ts b/packages/opencode/test/server/project-copy.test.ts index 4c40908f1f..83f5173ed2 100644 --- a/packages/opencode/test/server/project-copy.test.ts +++ b/packages/opencode/test/server/project-copy.test.ts @@ -21,7 +21,7 @@ afterEach(async () => { }) 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( Layer.mergeAll( LayerNode.compile(LayerNode.group([FSUtil.node, Database.node, Snapshot.node])), diff --git a/packages/opencode/test/server/project-init-git.test.ts b/packages/opencode/test/server/project-init-git.test.ts index dc5f92cd6e..f469d8bb04 100644 --- a/packages/opencode/test/server/project-init-git.test.ts +++ b/packages/opencode/test/server/project-init-git.test.ts @@ -20,7 +20,7 @@ afterEach(async () => { }) 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( Layer.mergeAll(LayerNode.compile(LayerNode.group([FSUtil.node, Snapshot.node])), testInstanceStore, httpApiLayer),