diff --git a/packages/opencode/test/agent/plugin-agent-regression.test.ts b/packages/opencode/test/agent/plugin-agent-regression.test.ts index a9a26c7226..0249aaeb84 100644 --- a/packages/opencode/test/agent/plugin-agent-regression.test.ts +++ b/packages/opencode/test/agent/plugin-agent-regression.test.ts @@ -1,16 +1,15 @@ import { expect } from "bun:test" -import { FSUtil } from "@opencode-ai/core/fs-util" -import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services" -import { Effect, Layer } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { Npm } from "@opencode-ai/core/npm" +import { Effect } from "effect" import path from "path" import { pathToFileURL } from "url" import { Agent } from "../../src/agent/agent" -import { EventV2Bridge } from "../../src/event-v2-bridge" -import { Config } from "../../src/config/config" -import { Env } from "../../src/env" +import { Account } from "../../src/account/account" +import { Auth } from "../../src/auth" import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Plugin } from "../../src/plugin" +import { Provider } from "../../src/provider/provider" +import { Skill } from "../../src/skill" import { AccountTest } from "../fake/account" import { AuthTest } from "../fake/auth" import { NpmTest } from "../fake/npm" @@ -18,6 +17,8 @@ import { ProviderTest } from "../fake/provider" import { SkillTest } from "../fake/skill" import { testEffect } from "../lib/effect" import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" // `it.instance` skips InstanceBootstrap so LSP / MCP don't spin up — those // services hang during scope teardown on Windows and aren't needed @@ -25,30 +26,16 @@ import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants" const pluginUrl = pathToFileURL(path.join(import.meta.dir, "..", "fixture", "agent-plugin.ts")).href const provider = ProviderTest.fake() -const configLayer = Config.layer.pipe( - Layer.provide(FSUtil.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(AuthTest.empty), - Layer.provide(AccountTest.empty), - Layer.provide(NpmTest.noop), - Layer.provide(FetchHttpClient.layer), +const it = testEffect( + AppNodeBuilder.build(LayerNode.group([Agent.node, Plugin.node]), [ + [Auth.node, AuthTest.empty], + [Account.node, AccountTest.empty], + [Npm.node, NpmTest.noop], + [Provider.node, provider.layer], + [Skill.node, SkillTest.empty], + [RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true })], + ]), ) -const pluginLayer = Plugin.layer.pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(configLayer), - Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), -) -const agentLayer = Agent.layer.pipe( - Layer.provide(configLayer), - Layer.provide(AuthTest.empty), - Layer.provide(SkillTest.empty), - Layer.provide(provider.layer), - Layer.provide(pluginLayer), - Layer.provide(locationServiceMapLayer), - Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), -) - -const it = testEffect(Layer.mergeAll(agentLayer, pluginLayer)) it.instance( "plugin-registered agents appear in Agent.list", diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index 0b680bc91a..36385ca56b 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -5,9 +5,8 @@ import Http from "node:http" import path from "node:path" import { NodeHttpServer } from "@effect/platform-node" import { Effect, Exit, Fiber, Layer, Schema } from "effect" -import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { eq } from "drizzle-orm" -import { FSUtil } from "@opencode-ai/core/fs-util" import { GlobalBus, type GlobalEvent } from "@/bus/global" import { Database } from "@opencode-ai/core/database/database" import { ProjectV2 } from "@opencode-ai/core/project" @@ -16,6 +15,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema" import { Session as SessionNs } from "@/session/session" import { SessionID } from "@/session/schema" import { SessionTable } from "@opencode-ai/core/session/sql" +import { SessionProjector } from "@opencode-ai/core/session/projector" import { EventSequenceTable } from "@opencode-ai/core/event/sql" import { resetDatabase } from "../fixture/db" import { disposeAllInstances, provideTmpdirInstance, requireInstance, TestInstance } from "../fixture/fixture" @@ -27,13 +27,10 @@ import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control- import * as Workspace from "../../src/control-plane/workspace" import { InstanceStore } from "@/project/instance-store" import { InstanceBootstrap } from "@/project/bootstrap" -import { Auth } from "@/auth" -import { SessionPrompt } from "@/session/prompt" -import { Project } from "@/project/project" -import { Vcs } from "@/project/vcs" import { RuntimeFlags } from "@/effect/runtime-flags" -import { EventV2Bridge } from "@/event-v2-bridge" import { Ripgrep } from "@opencode-ai/core/ripgrep" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" const originalEnv = { OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT, @@ -44,26 +41,24 @@ const originalEnv = { } const workspaceLayer = (experimentalWorkspaces: boolean) => - Workspace.layer.pipe( - Layer.provide(Auth.defaultLayer), - Layer.provide(SessionNs.defaultLayer), - Layer.provide(SessionPrompt.defaultLayer), - Layer.provide(Project.defaultLayer), - Layer.provide(Vcs.defaultLayer), - Layer.provide(Database.defaultLayer), - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(FetchHttpClient.layer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })), - Layer.provide(Ripgrep.defaultLayer), - Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))), + AppNodeBuilder.build( + LayerNode.group([ + Workspace.node, + SessionNs.node, + SessionProjector.node, + Database.node, + InstanceStore.node, + Ripgrep.node, + ]), + [ + [RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })], + [InstanceBootstrap.node, Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))], + ], ) const testServerLayer = Layer.mergeAll( NodeHttpServer.layer(Http.createServer, { host: "127.0.0.1", port: 0 }), workspaceLayer(true), - SessionNs.defaultLayer, - Database.defaultLayer, ) const it = testEffect(testServerLayer) diff --git a/packages/opencode/test/plugin/trigger.test.ts b/packages/opencode/test/plugin/trigger.test.ts index 7bd9e33527..07fd8e2f21 100644 --- a/packages/opencode/test/plugin/trigger.test.ts +++ b/packages/opencode/test/plugin/trigger.test.ts @@ -1,14 +1,11 @@ import { describe, expect } from "bun:test" -import { Effect, Layer } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { Effect } from "effect" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { FSUtil } from "@opencode-ai/core/fs-util" -import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { Npm } from "@opencode-ai/core/npm" import path from "path" import { pathToFileURL } from "url" -import { EventV2Bridge } from "../../src/event-v2-bridge" -import { Config } from "../../src/config/config" -import { Env } from "../../src/env" +import { Account } from "../../src/account/account" +import { Auth } from "../../src/auth" import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Plugin } from "../../src/plugin/index" @@ -19,25 +16,16 @@ import { AuthTest } from "../fake/auth" import { NpmTest } from "../fake/npm" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" -const configLayer = Config.layer.pipe( - Layer.provide(EffectFlock.defaultLayer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(AuthTest.empty), - Layer.provide(AccountTest.empty), - Layer.provide(NpmTest.noop), - Layer.provide(FetchHttpClient.layer), -) const it = testEffect( - Layer.mergeAll( - Plugin.layer.pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(configLayer), - Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), - ), - CrossSpawnSpawner.defaultLayer, - ), + AppNodeBuilder.build(LayerNode.group([Plugin.node, CrossSpawnSpawner.node]), [ + [Auth.node, AuthTest.empty], + [Account.node, AccountTest.empty], + [Npm.node, NpmTest.noop], + [RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true })], + ]), ) const systemHook = "experimental.chat.system.transform" diff --git a/packages/opencode/test/session/llm-native-recorded.test.ts b/packages/opencode/test/session/llm-native-recorded.test.ts index d17d7f8e5a..7ca09910bf 100644 --- a/packages/opencode/test/session/llm-native-recorded.test.ts +++ b/packages/opencode/test/session/llm-native-recorded.test.ts @@ -1,6 +1,5 @@ import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { SessionV1 } from "@opencode-ai/core/v1/session" -import { FSUtil } from "@opencode-ai/core/fs-util" import { ModelsDev } from "@opencode-ai/core/models-dev" import { HttpRecorder } from "@opencode-ai/http-recorder" import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal" @@ -10,14 +9,11 @@ import { Effect, Layer, Option, Schema, Stream } from "effect" import path from "node:path" import z from "zod" import { Auth } from "@/auth" -import { Config } from "@/config/config" -import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" import { Filesystem } from "@/util/filesystem" import { LLMEvent, LLMResponse } from "@opencode-ai/llm" -import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" -import { Env } from "@/env" +import { RequestExecutor } from "@opencode-ai/llm/route" import { RuntimeFlags } from "@/effect/runtime-flags" import type { Agent } from "../../src/agent/agent" import { LLM } from "../../src/session/llm" @@ -26,6 +22,9 @@ import { TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings") @@ -240,7 +239,7 @@ const redactRecordedBody = (body: string) => function authLayer(scenario: RecordedScenario) { const replayAuth = shouldRecord ? scenario.recordAuth?.() : scenario.replayAuth - if (!replayAuth) return Auth.defaultLayer + if (!replayAuth) return undefined return Layer.mock(Auth.Service)({ get: (providerID) => Effect.succeed(providerID === scenario.providerID ? replayAuth : undefined), all: () => Effect.succeed({ [scenario.providerID]: replayAuth }), @@ -262,15 +261,6 @@ const modelsFixture = Filesystem.readJson>( function recordedNativeLLMLayer(scenario: RecordedScenario) { const auth = authLayer(scenario) - const provider = Provider.layer.pipe( - Layer.provide(FSUtil.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(auth), - Layer.provide(Plugin.defaultLayer), - Layer.provide(ModelsDev.defaultLayer), - Layer.provide(RuntimeFlags.defaultLayer), - ) // Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real. const metadata = { provider: scenario.providerID, @@ -290,21 +280,11 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) { redactor: HttpRecorderInternal.Redactor.make(redact), }) : HttpRecorder.http(scenario.cassette, { directory: FIXTURES_DIR, metadata, redact }) - const recordedClient = LLMClient.layer.pipe( - Layer.provide(Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer)), - ) - - return Layer.mergeAll( - provider, - LLM.layer.pipe( - Layer.provide(auth), - Layer.provide(Config.defaultLayer), - Layer.provide(provider), - Layer.provide(Plugin.defaultLayer), - Layer.provide(recordedClient), - Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: true })), - ), - ) + return AppNodeBuilder.build(LayerNode.group([Provider.node, LLM.node]), [ + [LayerNodePlatform.requestExecutor, RequestExecutor.layer.pipe(Layer.provide(recordedHttp))], + [RuntimeFlags.node, RuntimeFlags.layer({ experimentalNativeLlm: true })], + ...(auth ? ([[Auth.node, auth]] as const) : []), + ]) } const writeConfig = (directory: string, scenario: RecordedScenario, model: ModelsDev.Provider["models"][string]) => diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 868477a910..beb41574c3 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -9,13 +9,10 @@ import { InstanceRef } from "../../src/effect/instance-ref" import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import z from "zod" import { LLM } from "../../src/session/llm" -import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" -import { Auth } from "@/auth" -import { Config } from "@/config/config" +import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route" import { Provider } from "@/provider/provider" import { ProviderTransform } from "@/provider/transform" import { ModelsDev } from "@opencode-ai/core/models-dev" -import { Plugin } from "@/plugin" import { testEffect } from "../lib/effect" import type { Agent } from "../../src/agent/agent" @@ -27,6 +24,9 @@ import { LLMAISDK } from "@/session/llm/ai-sdk" import { Session as SessionNs } from "@/session/session" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" type ConfigModel = NonNullable[string]["models"]>[string] @@ -52,15 +52,13 @@ const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: stri } } -const it = testEffect(Layer.mergeAll(LLM.defaultLayer, Provider.defaultLayer)) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([LLM.node, Provider.node]))) // LLM.stream returns a Stream, not an Effect, so we can't use the serviceUse proxy. const drain = (input: LLM.StreamInput) => LLM.Service.use((svc) => svc.stream(input).pipe(Stream.runDrain)) -// drainWith builds an isolated runtime so the custom layer fully owns LLM and -// its transitive deps — `Effect.provide(layer)` over an existing runtime layers -// the new services on top, but transitive Service overrides (e.g. RequestExecutor) -// resolved through the outer LLM.defaultLayer leak through. +// drainWith builds an isolated runtime so custom replacements fully own LLM and +// its transitive deps. const drainWith = (layer: Layer.Layer, input: LLM.StreamInput) => Effect.gen(function* () { const ctx = yield* InstanceRef @@ -75,15 +73,14 @@ const drainWith = (layer: Layer.Layer, input: LLM.StreamInput) => ) }) -function llmLayerWithExecutor(executor: Layer.Layer, flags: Partial = {}) { - return LLM.layer.pipe( - Layer.provide(Auth.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Provider.defaultLayer), - Layer.provide(Plugin.defaultLayer), - Layer.provide(LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(executor, WebSocketExecutor.layer)))), - Layer.provide(RuntimeFlags.layer(flags)), - ) +function llmLayerWithExecutor(options: { + executor?: Layer.Layer + flags?: Partial +} = {}) { + return AppNodeBuilder.build(LLM.node, [ + [RuntimeFlags.node, RuntimeFlags.layer(options.flags)], + ...(options.executor ? ([[LayerNodePlatform.requestExecutor, options.executor]] as const) : []), + ]) } describe("session.llm.hasToolCalls", () => { @@ -1129,14 +1126,10 @@ describe("session.llm.stream", () => { } satisfies Agent.Info yield* drainWith( - LLM.layer.pipe( - Layer.provide(Auth.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Provider.defaultLayer), - Layer.provide(Plugin.defaultLayer), - Layer.provide(failingNativeClient), - Layer.provide(RuntimeFlags.layer({ experimentalNativeLlm: false })), - ), + AppNodeBuilder.build(LLM.node, [ + [LayerNodePlatform.llmClient, failingNativeClient], + [RuntimeFlags.node, RuntimeFlags.layer({ experimentalNativeLlm: false })], + ]), { user: { id: MessageID.make("msg_user-native-flag-off"), @@ -1199,7 +1192,7 @@ describe("session.llm.stream", () => { temperature: 0.2, } satisfies Agent.Info - yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), { + yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), { user: { id: MessageID.make("msg_user-native"), sessionID, @@ -1282,7 +1275,7 @@ describe("session.llm.stream", () => { permission: [{ permission: "*", pattern: "*", action: "allow" }], } satisfies Agent.Info - yield* drainWith(llmLayerWithExecutor(executor, { experimentalNativeLlm: true }), { + yield* drainWith(llmLayerWithExecutor({ executor, flags: { experimentalNativeLlm: true } }), { user: { id: MessageID.make("msg_user-native-injected-tool"), sessionID, @@ -1371,7 +1364,7 @@ describe("session.llm.stream", () => { permission: [{ permission: "*", pattern: "*", action: "allow" }], } satisfies Agent.Info - yield* drainWith(llmLayerWithExecutor(RequestExecutor.defaultLayer, { experimentalNativeLlm: true }), { + yield* drainWith(llmLayerWithExecutor({ flags: { experimentalNativeLlm: true } }), { user: { id: MessageID.make("msg_user-native-tool"), sessionID,