Sfoglia il codice sorgente

refactor(core): convert more opencode tests to nodes (#34464)

James Long 2 mesi fa
parent
commit
d4fd528152
45 ha cambiato i file con 209 aggiunte e 203 eliminazioni
  1. 5 0
      packages/opencode/src/acp/directory.ts
  2. 12 0
      packages/opencode/src/acp/usage.ts
  3. 6 3
      packages/opencode/test/account/service.test.ts
  4. 6 4
      packages/opencode/test/acp/directory.test.ts
  5. 20 17
      packages/opencode/test/acp/usage.test.ts
  6. 4 10
      packages/opencode/test/agent/agent.test.ts
  7. 3 2
      packages/opencode/test/cli/run/variant.shared.test.ts
  8. 2 1
      packages/opencode/test/config/tui.test.ts
  9. 2 1
      packages/opencode/test/effect/instance-state.test.ts
  10. 3 2
      packages/opencode/test/format/format.test.ts
  11. 9 2
      packages/opencode/test/installation/installation.test.ts
  12. 7 8
      packages/opencode/test/lsp/index.test.ts
  13. 3 2
      packages/opencode/test/mcp/auth.test.ts
  14. 2 10
      packages/opencode/test/mcp/oauth-auto-connect.test.ts
  15. 2 7
      packages/opencode/test/mcp/oauth-browser.test.ts
  16. 2 1
      packages/opencode/test/plugin/auth-override.test.ts
  17. 2 1
      packages/opencode/test/plugin/loader-shared.test.ts
  18. 5 9
      packages/opencode/test/provider/provider.test.ts
  19. 5 1
      packages/opencode/test/server/global-session-list.test.ts
  20. 2 1
      packages/opencode/test/server/httpapi-experimental.test.ts
  21. 2 1
      packages/opencode/test/server/httpapi-provider.test.ts
  22. 2 1
      packages/opencode/test/server/httpapi-schema-error-body.test.ts
  23. 2 1
      packages/opencode/test/server/httpapi-sync.test.ts
  24. 2 1
      packages/opencode/test/server/negative-tokens-regression.test.ts
  25. 4 3
      packages/opencode/test/server/project-copy.test.ts
  26. 4 3
      packages/opencode/test/server/project-init-git.test.ts
  27. 2 1
      packages/opencode/test/server/session-actions.test.ts
  28. 2 1
      packages/opencode/test/server/session-diff-missing-patch.test.ts
  29. 2 1
      packages/opencode/test/server/session-messages.test.ts
  30. 2 1
      packages/opencode/test/server/session-select.test.ts
  31. 9 8
      packages/opencode/test/session/system.test.ts
  32. 5 18
      packages/opencode/test/skill/skill.test.ts
  33. 4 3
      packages/opencode/test/snapshot/snapshot.test.ts
  34. 4 3
      packages/opencode/test/storage/storage.test.ts
  35. 2 8
      packages/opencode/test/tool/apply_patch.test.ts
  36. 2 8
      packages/opencode/test/tool/edit.test.ts
  37. 2 8
      packages/opencode/test/tool/glob.test.ts
  38. 2 8
      packages/opencode/test/tool/grep.test.ts
  39. 4 1
      packages/opencode/test/tool/lsp.test.ts
  40. 3 8
      packages/opencode/test/tool/read.test.ts
  41. 12 7
      packages/opencode/test/tool/shell.test.ts
  42. 2 3
      packages/opencode/test/tool/skill.test.ts
  43. 21 14
      packages/opencode/test/tool/task.test.ts
  44. 8 2
      packages/opencode/test/tool/webfetch.test.ts
  45. 3 8
      packages/opencode/test/tool/write.test.ts

+ 5 - 0
packages/opencode/src/acp/directory.ts

@@ -2,6 +2,7 @@ import { Agent } from "@/agent/agent"
 import { Command } from "@/command"
 import { InstanceRef } from "@/effect/instance-ref"
 import { InstanceStore } from "@/project/instance-store"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { Provider } from "@/provider/provider"
@@ -207,4 +208,8 @@ export const defaultLayer = layer.pipe(
   Layer.provide(InstanceStore.defaultLayer),
 )
 
+export const loaderNode = LayerNode.make({ service: Loader, layer: loaderLayer, deps: [Provider.node, Agent.node, Command.node, InstanceStore.node] })
+
+export const node = LayerNode.make({ service: Service, layer, deps: [loaderNode] })
+
 export * as Directory from "./directory"

+ 12 - 0
packages/opencode/src/acp/usage.ts

@@ -2,6 +2,8 @@ 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 { InstanceStore } from "@/project/instance-store"
+import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { Provider } from "@/provider/provider"
@@ -229,4 +231,14 @@ export const defaultLayer = layer.pipe(
   Layer.provide(InstanceStore.defaultLayer),
 )
 
+export const messageLoaderNode = LayerNode.unbound(MessageLoader, Node.tags.values.global)
+
+export const contextLimitLoaderNode = makeGlobalNode({
+  service: ContextLimitLoader,
+  layer: contextLimitLoaderLayer,
+  deps: [Provider.node, InstanceStore.node],
+})
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [messageLoaderNode, contextLimitLoaderNode] })
+
 export * as UsageService from "./usage"

+ 6 - 3
packages/opencode/test/account/service.test.ts

@@ -1,4 +1,6 @@
 import { expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
 import { Duration, Effect, Layer, Option, Schema } from "effect"
 import { sql } from "drizzle-orm"
 import { HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http"
@@ -25,15 +27,16 @@ const truncate = Layer.effectDiscard(
     yield* db.run(sql`DELETE FROM account_state`)
     yield* db.run(sql`DELETE FROM account`)
   }),
-).pipe(Layer.provide(Database.defaultLayer))
+)
+const truncateNode = LayerNode.make({ name: "truncate-account", layer: truncate, deps: [Database.node] })
 
-const it = testEffect(Layer.merge(AccountRepo.defaultLayer, truncate))
+const it = testEffect(LayerNode.compile(LayerNode.group([AccountRepo.node, truncateNode])))
 
 const insideEagerRefreshWindow = Duration.toMillis(Duration.minutes(1))
 const outsideEagerRefreshWindow = Duration.toMillis(Duration.minutes(10))
 
 const live = (client: HttpClient.HttpClient) =>
-  Account.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client)))
+  LayerNode.compile(Account.node, [[httpClient, Layer.succeed(HttpClient.HttpClient, client)]])
 
 const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
   HttpClientResponse.fromWeb(

+ 6 - 4
packages/opencode/test/acp/directory.test.ts

@@ -1,6 +1,7 @@
 import { describe, expect } from "bun:test"
 import { Directory } from "@/acp/directory"
 import { Command } from "@/command"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { Provider } from "@/provider/provider"
@@ -83,8 +84,9 @@ const snapshot = (directory: string) => {
 }
 
 const fakeLayer = (calls: string[]) =>
-  Directory.layer.pipe(
-    Layer.provide(
+  LayerNode.compile(Directory.node, [
+    [
+      Directory.loaderNode,
       Layer.succeed(
         Directory.Loader,
         Directory.Loader.of({
@@ -95,8 +97,8 @@ const fakeLayer = (calls: string[]) =>
             }),
         }),
       ),
-    ),
-  )
+    ],
+  ])
 
 describe("ACP directory snapshot", () => {
   it.effect("two concurrent callers share one load", () => {

+ 20 - 17
packages/opencode/test/acp/usage.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test } from "bun:test"
 import type { SessionNotification } from "@agentclientprotocol/sdk"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { UsageService } from "@/acp/usage"
@@ -97,24 +98,26 @@ const fakeLayer = (input: {
   readonly messages?: Effect.Effect<readonly UsageService.SessionMessage[], unknown>
   readonly providers?: (directory: string) => Effect.Effect<Record<ProviderV2.ID, Provider.Info>, unknown>
 }) =>
-  UsageService.layer.pipe(
-    Layer.provide(
-      Layer.mergeAll(
-        Layer.succeed(
-          UsageService.MessageLoader,
-          UsageService.MessageLoader.of({
-            messages: () => input.messages ?? Effect.succeed([]),
-          }),
-        ),
-        Layer.succeed(
-          UsageService.ContextLimitLoader,
-          UsageService.ContextLimitLoader.of({
-            providers: input.providers ?? (() => Effect.succeed(providers())),
-          }),
-        ),
+  LayerNode.compile(UsageService.node, [
+    [
+      UsageService.messageLoaderNode,
+      Layer.succeed(
+        UsageService.MessageLoader,
+        UsageService.MessageLoader.of({
+          messages: () => input.messages ?? Effect.succeed([]),
+        }),
+      ),
+    ],
+    [
+      UsageService.contextLimitLoaderNode,
+      Layer.succeed(
+        UsageService.ContextLimitLoader,
+        UsageService.ContextLimitLoader.of({
+          providers: input.providers ?? (() => Effect.succeed(providers())),
+        }),
       ),
-    ),
-  )
+    ],
+  ])
 
 const connection = (updates: SessionNotification[]) => ({
   sessionUpdate(params: SessionNotification) {

+ 4 - 10
packages/opencode/test/agent/agent.test.ts

@@ -1,4 +1,5 @@
 import { afterEach, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Effect, Exit, Layer } from "effect"
 import path from "path"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
@@ -14,18 +15,11 @@ import { Plugin } from "../../src/plugin"
 import { Provider } from "../../src/provider/provider"
 import { Skill } from "../../src/skill"
 import { Truncate } from "../../src/tool/truncate"
-import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 
 const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
-  Agent.layer.pipe(
-    Layer.provide(Plugin.defaultLayer),
-    Layer.provide(Provider.defaultLayer),
-    Layer.provide(Auth.defaultLayer),
-    Layer.provide(Config.defaultLayer),
-    Layer.provide(Skill.defaultLayer),
-    Layer.provide(locationServiceMapLayer),
-    Layer.provide(RuntimeFlags.layer(flags)),
-  )
+  LayerNode.compile(LayerNode.group([Agent.node, Plugin.node, Provider.node, Auth.node, Config.node, Skill.node, RuntimeFlags.node]), [
+    [RuntimeFlags.node, RuntimeFlags.layer(flags)],
+  ])
 
 const it = testEffect(agentLayer())
 

+ 3 - 2
packages/opencode/test/cli/run/variant.shared.test.ts

@@ -1,5 +1,6 @@
 import path from "path"
 import { NodeFileSystem } from "@effect/platform-node"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { describe, expect, test } from "bun:test"
 import { Effect, FileSystem, Layer } from "effect"
@@ -98,7 +99,7 @@ function userMessage(
   }
 }
 
-const it = testEffect(Layer.mergeAll(FSUtil.defaultLayer, NodeFileSystem.layer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(FSUtil.node), NodeFileSystem.layer))
 
 function remap(root: string, file: string) {
   if (file === Global.Path.state) {
@@ -123,7 +124,7 @@ function remappedFs(root: string) {
         writeJson: (file, data, mode) => fs.writeJson(remap(root, file), data, mode),
       })
     }),
-  ).pipe(Layer.provide(FSUtil.defaultLayer))
+  ).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
 }
 
 describe("run variant shared", () => {

+ 2 - 1
packages/opencode/test/config/tui.test.ts

@@ -1,6 +1,7 @@
 import { expect } from "bun:test"
 import path from "path"
 import { pathToFileURL } from "url"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Global } from "@opencode-ai/core/global"
@@ -11,7 +12,7 @@ import { TuiConfig } from "../../src/config/tui"
 import { TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Config.defaultLayer, FSUtil.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Config.node, FSUtil.node])))
 const winIt = process.platform === "win32" ? it.instance : it.instance.skip
 
 const globalConfigFiles = ["opencode.json", "opencode.jsonc", "tui.json", "tui.jsonc"].map((file) =>

+ 2 - 1
packages/opencode/test/effect/instance-state.test.ts

@@ -1,5 +1,6 @@
 import { expect } from "bun:test"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { $ } from "bun"
 import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
 import { InstanceState } from "@/effect/instance-state"
@@ -12,7 +13,7 @@ import {
 } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(CrossSpawnSpawner.node), testInstanceStoreLayer))
 
 const access = <A, E>(state: InstanceState.InstanceState<A, E>, dir: string) =>
   InstanceState.get(state).pipe(provideInstanceEffect(dir))

+ 3 - 2
packages/opencode/test/format/format.test.ts

@@ -1,5 +1,6 @@
 import { NodeFileSystem } from "@effect/platform-node"
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { provideTmpdirInstance, testInstanceStoreLayer, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
@@ -7,7 +8,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Format } from "../../src/format"
 import * as Formatter from "../../src/format/formatter"
 
-const it = testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])), NodeFileSystem.layer))
 
 describe("Format", () => {
   it.instance("status() returns empty list when no formatters are configured", () =>
@@ -106,7 +107,7 @@ describe("Format", () => {
   )
 
   testEffect(
-    Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer),
+    Layer.mergeAll(LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])), NodeFileSystem.layer, testInstanceStoreLayer),
   ).live("status() initializes formatter state per directory", () =>
     Effect.gen(function* () {
       const a = yield* provideTmpdirInstance(() => Format.use.status(), {

+ 9 - 2
packages/opencode/test/installation/installation.test.ts

@@ -1,4 +1,7 @@
 import { describe, expect } from "bun:test"
+import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
 import { Effect, Layer, Stream } from "effect"
 import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
@@ -52,8 +55,12 @@ function testLayer(
   httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
   spawnHandler?: (cmd: string, args: readonly string[]) => string | { code: number; stdout?: string; stderr?: string },
 ) {
-  const appProcess = AppProcess.layer.pipe(Layer.provide(mockSpawner(spawnHandler)))
-  return Installation.layer.pipe(Layer.provide(mockHttpClient(httpHandler)), Layer.provide(appProcess))
+  const spawnerNode = makeGlobalNode({ service: ChildProcessSpawner.ChildProcessSpawner, layer: mockSpawner(spawnHandler), deps: [] })
+  const appProcessNode = makeGlobalNode({ service: AppProcess.Service, layer: AppProcess.layer, deps: [spawnerNode] })
+  return LayerNode.compile(Installation.node, [
+    [httpClient, mockHttpClient(httpHandler)],
+    [AppProcess.node, appProcessNode],
+  ])
 }
 
 describe("installation", () => {

+ 7 - 8
packages/opencode/test/lsp/index.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, spyOn } from "bun:test"
 import path from "path"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Deferred, Effect, Layer } from "effect"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { Config } from "@/config/config"
@@ -11,19 +12,17 @@ import { TestInstance } from "../fixture/fixture"
 import { awaitWithTimeout, testEffect } from "../lib/effect"
 
 const lspLayer = (flags: Parameters<typeof RuntimeFlags.layer>[0] = {}) =>
-  LSP.layer.pipe(
-    Layer.provide(Config.defaultLayer),
-    Layer.provide(RuntimeFlags.layer(flags)),
-    Layer.provideMerge(EventV2Bridge.defaultLayer),
-  )
+  LayerNode.compile(LayerNode.group([LSP.node, Config.node, RuntimeFlags.node, EventV2Bridge.node]), [
+    [RuntimeFlags.node, RuntimeFlags.layer(flags)],
+  ])
 
-const it = testEffect(Layer.mergeAll(lspLayer(), CrossSpawnSpawner.defaultLayer))
+const it = testEffect(Layer.mergeAll(lspLayer(), LayerNode.compile(CrossSpawnSpawner.node)))
 const experimentalTyIt = testEffect(
-  Layer.mergeAll(lspLayer({ experimentalLspTy: true }), CrossSpawnSpawner.defaultLayer),
+  Layer.mergeAll(lspLayer({ experimentalLspTy: true }), LayerNode.compile(CrossSpawnSpawner.node)),
 )
 const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
 const disabledDownloadIt = testEffect(
-  Layer.mergeAll(lspLayer({ disableLspDownload: true }), CrossSpawnSpawner.defaultLayer),
+  Layer.mergeAll(lspLayer({ disableLspDownload: true }), LayerNode.compile(CrossSpawnSpawner.node)),
 )
 
 describe("lsp.spawn", () => {

+ 3 - 2
packages/opencode/test/mcp/auth.test.ts

@@ -1,5 +1,6 @@
 import { expect, test } from "bun:test"
 import { setTimeout as sleep } from "node:timers/promises"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
@@ -41,14 +42,14 @@ function authFile() {
             : fs.writeJson(file, value, mode),
       })
     }),
-  ).pipe(Layer.provide(FSUtil.defaultLayer))
+  ).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
 
   return { layer, raw: () => raw }
 }
 
 function authService(layer: Layer.Layer<FSUtil.Service>) {
   return McpAuth.Service.use((auth) => Effect.succeed(auth)).pipe(
-    Effect.provide(McpAuth.layer.pipe(Layer.provide(EffectFlock.defaultLayer), Layer.provide(layer))),
+    Effect.provide(LayerNode.compile(McpAuth.node, [[FSUtil.node, layer]])),
   )
 }
 

+ 2 - 10
packages/opencode/test/mcp/oauth-auto-connect.test.ts

@@ -1,4 +1,5 @@
 import { expect, mock, beforeEach } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { testEffect } from "../lib/effect"
 
@@ -155,16 +156,7 @@ const { FSUtil } = await import("@opencode-ai/core/fs-util")
 const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner")
 
 const mcpTest = testEffect(
-  Layer.mergeAll(
-    MCP.layer.pipe(
-      Layer.provide(McpAuth.defaultLayer),
-      Layer.provideMerge(EventV2Bridge.defaultLayer),
-      Layer.provide(Config.defaultLayer),
-      Layer.provide(CrossSpawnSpawner.defaultLayer),
-      Layer.provide(FSUtil.defaultLayer),
-    ),
-    McpAuth.defaultLayer,
-  ),
+  LayerNode.compile(LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node])),
 )
 
 const config = (name: string) => ({

+ 2 - 7
packages/opencode/test/mcp/oauth-browser.test.ts

@@ -1,5 +1,6 @@
 import { expect, mock, beforeEach } from "bun:test"
 import { EventEmitter } from "events"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Deferred, Effect, Layer, Option } from "effect"
 import { awaitWithTimeout, testEffect } from "../lib/effect"
 import type { MCP as MCPNS } from "../../src/mcp/index"
@@ -122,13 +123,7 @@ const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
 const { FSUtil } = await import("@opencode-ai/core/fs-util")
 const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner")
 const mcpTest = testEffect(
-  MCP.layer.pipe(
-    Layer.provide(McpAuth.defaultLayer),
-    Layer.provideMerge(EventV2Bridge.defaultLayer),
-    Layer.provide(Config.defaultLayer),
-    Layer.provide(CrossSpawnSpawner.defaultLayer),
-    Layer.provide(FSUtil.defaultLayer),
-  ),
+  LayerNode.compile(LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node])),
 )
 const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>
 

+ 2 - 1
packages/opencode/test/plugin/auth-override.test.ts

@@ -1,6 +1,7 @@
 import { describe, expect, test } from "bun:test"
 import path from "path"
 import { pathToFileURL } from "url"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
@@ -15,7 +16,7 @@ import { testEffect } from "../lib/effect"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 
-const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, FSUtil.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node])))
 
 function layer(directory: string, plugins: string[]) {
   return ProviderAuth.layer.pipe(

+ 2 - 1
packages/opencode/test/plugin/loader-shared.test.ts

@@ -1,4 +1,5 @@
 import { afterEach, describe, expect, spyOn } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import fs from "fs/promises"
 import path from "path"
@@ -20,7 +21,7 @@ afterEach(async () => {
   await disposeAllInstances()
 })
 
-const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, FSUtil.defaultLayer, testInstanceStoreLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node])), testInstanceStoreLayer))
 
 function withTmp<T, A, E, R>(
   init: (dir: string) => Promise<T>,

+ 5 - 9
packages/opencode/test/provider/provider.test.ts

@@ -1,6 +1,7 @@
 import { afterEach, expect, test } from "bun:test"
 import { mkdir, unlink } from "fs/promises"
 import path from "path"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { ModelsDev } from "@opencode-ai/core/models-dev"
 import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -57,14 +58,9 @@ afterEach(async () => {
 })
 
 const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
-  Provider.layer.pipe(
-    Layer.provide(FSUtil.defaultLayer),
-    Layer.provide(Env.defaultLayer),
-    Layer.provide(Config.defaultLayer),
-    Layer.provide(Auth.defaultLayer),
-    Layer.provide(Plugin.defaultLayer),
-    Layer.provide(ModelsDev.defaultLayer),
-    Layer.provide(RuntimeFlags.layer(flags)),
+  LayerNode.compile(
+    LayerNode.group([Provider.node, FSUtil.node, Env.node, Config.node, Auth.node, Plugin.node, ModelsDev.node, RuntimeFlags.node]),
+    [[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
   )
 
 const list = Provider.use.list()
@@ -77,7 +73,7 @@ const paid = (providers: Record<string, { models: Record<string, { cost: { input
 
 const languageBaseURL = (language: unknown) => (language as { config: { baseURL: string } }).config.baseURL
 
-const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node])))
 const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
 
 const alphaProviderConfig = {

+ 5 - 1
packages/opencode/test/server/global-session-list.test.ts

@@ -1,4 +1,6 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { SessionProjector } from "@opencode-ai/core/session/projector"
 import { Deferred, Effect, Layer } from "effect"
 import { Project } from "@/project/project"
 import { Session as SessionNs } from "@/session/session"
@@ -6,7 +8,9 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, Project.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const it = testEffect(
+  LayerNode.compile(LayerNode.group([SessionNs.node, SessionProjector.node, Project.node, CrossSpawnSpawner.node])),
+)
 
 const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) =>
   Effect.acquireRelease(SessionNs.use.create(input), (created) =>

+ 2 - 1
packages/opencode/test/server/httpapi-experimental.test.ts

@@ -1,4 +1,5 @@
 import { afterEach, describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Deferred, Effect, Fiber, Layer } from "effect"
 import { HttpClient, HttpClientResponse } from "effect/unstable/http"
 import { eq } from "drizzle-orm"
@@ -15,7 +16,7 @@ import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
-const it = testEffect(Layer.mergeAll(Session.defaultLayer, Database.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Database.node])), httpApiLayer))
 const testWorktreeMutations = process.platform === "win32" ? it.instance.skip : it.instance
 
 function request(path: string, directory: string, init: RequestInit = {}) {

+ 2 - 1
packages/opencode/test/server/httpapi-provider.test.ts

@@ -1,4 +1,5 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Effect, Layer } from "effect"
 import path from "path"
@@ -15,7 +16,7 @@ const testStateLayer = Layer.effectDiscard(
   ),
 )
 
-const it = testEffect(Layer.mergeAll(testStateLayer, FSUtil.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(testStateLayer, LayerNode.compile(FSUtil.node), httpApiLayer))
 const projectOptions = { config: { formatter: false, lsp: false } }
 const providerID = "test-oauth-parity"
 const oauthURL = "https://example.com/oauth"

+ 2 - 1
packages/opencode/test/server/httpapi-schema-error-body.test.ts

@@ -1,4 +1,5 @@
 import { afterEach, describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { HttpClientResponse } from "effect/unstable/http"
 import { eq } from "drizzle-orm"
@@ -16,7 +17,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
-const it = testEffect(Layer.mergeAll(Session.defaultLayer, Database.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Database.node])), httpApiLayer))
 
 const text = (response: HttpClientResponse.HttpClientResponse) => response.text
 

+ 2 - 1
packages/opencode/test/server/httpapi-sync.test.ts

@@ -1,4 +1,5 @@
 import { afterEach, describe, expect, mock } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Context, Effect, Layer } from "effect"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
@@ -11,7 +12,7 @@ import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
 const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
 const context = Context.empty() as Context.Context<unknown>
-const it = testEffect(Layer.mergeAll(Session.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(Session.node), httpApiLayer))
 
 afterEach(async () => {
   mock.restore()

+ 2 - 1
packages/opencode/test/server/negative-tokens-regression.test.ts

@@ -6,6 +6,7 @@
 // strict `NonNegativeInt` schema then made every load of the message list
 // fail to encode, killing Desktop boot for every user with such a row.
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { eq } from "drizzle-orm"
 
@@ -21,7 +22,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
-const it = testEffect(Layer.mergeAll(Session.defaultLayer, Database.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Database.node])), httpApiLayer))
 
 function seedNegativeTokenSession() {
   return Effect.gen(function* () {

+ 4 - 3
packages/opencode/test/server/project-copy.test.ts

@@ -2,12 +2,13 @@ import { afterEach, describe, expect } from "bun:test"
 import { $ } from "bun"
 import fs from "fs/promises"
 import path from "path"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { HttpClientResponse } from "effect/unstable/http"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Database } from "@opencode-ai/core/database/database"
 import { Snapshot } from "@/snapshot"
-import { InstanceBootstrap } from "@/project/bootstrap-service"
+import { InstanceBootstrap } from "@/project/bootstrap"
 import { InstanceStore } from "@/project/instance-store"
 import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
@@ -20,9 +21,9 @@ afterEach(async () => {
 })
 
 const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
-const testInstanceStore = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
+const testInstanceStore = LayerNode.compile(InstanceStore.node, [[InstanceBootstrap.node, noopBootstrap]])
 const it = testEffect(
-  Layer.mergeAll(FSUtil.defaultLayer, Database.defaultLayer, Snapshot.defaultLayer, testInstanceStore, httpApiLayer),
+  Layer.mergeAll(LayerNode.compile(LayerNode.group([FSUtil.node, Database.node, Snapshot.node])), testInstanceStore, httpApiLayer),
 )
 
 function request(directory: string, url: string, init: RequestInit = {}) {

+ 4 - 3
packages/opencode/test/server/project-init-git.test.ts

@@ -1,10 +1,11 @@
 import { afterEach, describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Effect, Layer } from "effect"
 import { HttpClientResponse } from "effect/unstable/http"
 import path from "path"
 import { InstanceRef } from "../../src/effect/instance-ref"
-import { InstanceBootstrap } from "../../src/project/bootstrap-service"
+import { InstanceBootstrap } from "../../src/project/bootstrap"
 import { InstanceStore } from "../../src/project/instance-store"
 import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
 import { Snapshot } from "../../src/snapshot"
@@ -19,9 +20,9 @@ afterEach(async () => {
 })
 
 const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
-const testInstanceStore = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
+const testInstanceStore = LayerNode.compile(InstanceStore.node, [[InstanceBootstrap.node, noopBootstrap]])
 
-const it = testEffect(Layer.mergeAll(FSUtil.defaultLayer, Snapshot.defaultLayer, testInstanceStore, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([FSUtil.node, Snapshot.node])), testInstanceStore, httpApiLayer))
 
 function request(directory: string, url: string, init: RequestInit = {}) {
   return requestInDirectory(url, directory, init)

+ 2 - 1
packages/opencode/test/server/session-actions.test.ts

@@ -1,11 +1,12 @@
 import { afterEach, describe, expect, mock } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { Session as SessionNs } from "@/session/session"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
-const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), httpApiLayer))
 
 afterEach(async () => {
   mock.restore()

+ 2 - 1
packages/opencode/test/server/session-diff-missing-patch.test.ts

@@ -11,6 +11,7 @@
  * asserts that GET /session/<id>/diff returns 200 with empty data.
  */
 import { afterEach, describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { SessionPaths } from "@/server/routes/instance/httpapi/groups/session"
 import { Session } from "@/session/session"
@@ -24,7 +25,7 @@ import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
-const it = testEffect(Layer.mergeAll(Session.defaultLayer, Storage.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Storage.node])), httpApiLayer))
 
 afterEach(async () => {
   await disposeAllInstances()

+ 2 - 1
packages/opencode/test/server/session-messages.test.ts

@@ -1,5 +1,6 @@
 import { afterEach, describe, expect } from "bun:test"
 import { SessionV1 } from "@opencode-ai/core/v1/session"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { HttpClientResponse } from "effect/unstable/http"
 import { Session as SessionNs } from "@/session/session"
@@ -12,7 +13,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
-const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), httpApiLayer))
 
 const model = {
   providerID: ProviderV2.ID.make("test"),

+ 2 - 1
packages/opencode/test/server/session-select.test.ts

@@ -1,11 +1,12 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { Session } from "@/session/session"
 import { TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
 
-const it = testEffect(Layer.mergeAll(Session.defaultLayer, httpApiLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(Session.node), httpApiLayer))
 
 describe("tui.selectSession endpoint", () => {
   it.instance(

+ 9 - 8
packages/opencode/test/session/system.test.ts

@@ -1,4 +1,5 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import type { Agent } from "../../src/agent/agent"
 import { NamedError } from "@opencode-ai/core/util/error"
@@ -6,7 +7,6 @@ import { Skill } from "../../src/skill"
 import { Permission } from "../../src/permission"
 import { SystemPrompt } from "../../src/session/system"
 import { MCP } from "../../src/mcp"
-import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { testEffect } from "../lib/effect"
 
 const skills: Skill.Info[] = [
@@ -43,9 +43,9 @@ const build: Agent.Info = {
 }
 
 const it = testEffect(
-  SystemPrompt.layer.pipe(
-    Layer.provide(locationServiceMapLayer),
-    Layer.provide(
+  LayerNode.compile(SystemPrompt.node, [
+    [
+      MCP.node,
       Layer.mock(MCP.Service, {
         instructions: () =>
           Effect.succeed([
@@ -61,8 +61,9 @@ const it = testEffect(
             },
           ]),
       }),
-    ),
-    Layer.provide(
+    ],
+    [
+      Skill.node,
       Layer.succeed(
         Skill.Service,
         Skill.Service.of({
@@ -77,8 +78,8 @@ const it = testEffect(
           available: () => Effect.succeed(skills),
         }),
       ),
-    ),
-  ),
+    ],
+  ]),
 )
 
 describe("session.system", () => {

+ 5 - 18
packages/opencode/test/skill/skill.test.ts

@@ -1,4 +1,5 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { Skill } from "../../src/skill"
 import { Discovery } from "../../src/skill/discovery"
@@ -13,33 +14,19 @@ import { testEffect } from "../lib/effect"
 import path from "path"
 import fs from "fs/promises"
 
-const node = CrossSpawnSpawner.defaultLayer
+const node = LayerNode.compile(CrossSpawnSpawner.node)
 
-const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node, testInstanceStoreLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(Skill.node), node, testInstanceStoreLayer))
 const itWithoutClaudeCodeSkills = testEffect(
   Layer.mergeAll(
-    Skill.layer.pipe(
-      Layer.provide(Discovery.defaultLayer),
-      Layer.provide(Config.defaultLayer),
-      Layer.provide(EventV2Bridge.defaultLayer),
-      Layer.provide(FSUtil.defaultLayer),
-      Layer.provide(Global.layer),
-      Layer.provide(RuntimeFlags.layer({ disableClaudeCodeSkills: true })),
-    ),
+    LayerNode.compile(Skill.node, [[RuntimeFlags.node, RuntimeFlags.layer({ disableClaudeCodeSkills: true })]]),
     node,
     testInstanceStoreLayer,
   ),
 )
 const itWithoutExternalSkills = testEffect(
   Layer.mergeAll(
-    Skill.layer.pipe(
-      Layer.provide(Discovery.defaultLayer),
-      Layer.provide(Config.defaultLayer),
-      Layer.provide(EventV2Bridge.defaultLayer),
-      Layer.provide(FSUtil.defaultLayer),
-      Layer.provide(Global.layer),
-      Layer.provide(RuntimeFlags.layer({ disableExternalSkills: true })),
-    ),
+    LayerNode.compile(Skill.node, [[RuntimeFlags.node, RuntimeFlags.layer({ disableExternalSkills: true })]]),
     node,
     testInstanceStoreLayer,
   ),

+ 4 - 3
packages/opencode/test/snapshot/snapshot.test.ts

@@ -1,6 +1,7 @@
 import { afterEach, expect } from "bun:test"
 import { $ } from "bun"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import fs from "fs/promises"
 import path from "path"
@@ -15,7 +16,7 @@ import {
 } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, FSUtil.defaultLayer, testInstanceStoreLayer))
+const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Snapshot.node, FSUtil.node])), testInstanceStoreLayer))
 // Windows forbids both * and : in directory names.
 const nonWindowsIt = process.platform === "win32" ? it.live.skip : it.live
 
@@ -74,11 +75,11 @@ const withTrackedSnapshot = <A, E, R>(
   })
 
 const bootstrapScoped = Effect.fn("SnapshotTest.bootstrapScoped")(function* () {
-  const dir = yield* tmpdirScoped({ git: true }).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer))
+  const dir = yield* tmpdirScoped({ git: true }).pipe(Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)))
   return { path: dir, extra: yield* initialize(dir) }
 })
 
-const scopedGitTmpdir = () => tmpdirScoped({ git: true }).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer))
+const scopedGitTmpdir = () => tmpdirScoped({ git: true }).pipe(Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)))
 
 const cleanupWorktree = (repo: string, worktree: string, files: string[] = []) =>
   Effect.promise(async () => {

+ 4 - 3
packages/opencode/test/storage/storage.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect } from "bun:test"
 import path from "path"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Exit, Layer } from "effect"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -11,7 +12,7 @@ import { testEffect } from "../lib/effect"
 
 const dir = path.join(Global.Path.data, "storage")
 
-const it = testEffect(Layer.mergeAll(Storage.defaultLayer, FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Storage.node, FSUtil.node, CrossSpawnSpawner.node])))
 
 const scope = Effect.fnUntraced(function* () {
   const root = ["storage_test", crypto.randomUUID()]
@@ -50,14 +51,14 @@ function remappedFs(root: string) {
           fs.glob(pattern, options?.cwd ? { ...options, cwd: remap(root, options.cwd) } : options),
       })
     }),
-  ).pipe(Layer.provide(FSUtil.defaultLayer))
+  ).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
 }
 
 // Layer.fresh forces a new Storage instance — without it, Effect's in-test layer cache
 // returns the outer testEffect's Storage (which uses the real FSUtil), not a new
 // one built on top of remappedFs.
 const remappedStorage = (root: string) =>
-  Layer.fresh(Storage.layer.pipe(Layer.provide(remappedFs(root)), Layer.provide(Git.defaultLayer)))
+  Layer.fresh(LayerNode.compile(Storage.node, [[FSUtil.node, remappedFs(root)]]))
 
 describe("Storage", () => {
   it.live("round-trips JSON content", () =>

+ 2 - 8
packages/opencode/test/tool/apply_patch.test.ts

@@ -1,6 +1,7 @@
 import { describe, expect } from "bun:test"
 import path from "path"
 import * as fs from "fs/promises"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Effect, Exit, Layer } from "effect"
 import { ApplyPatchTool } from "../../src/tool/apply_patch"
 import { LSP } from "@/lsp/lsp"
@@ -14,14 +15,7 @@ import { SessionID, MessageID } from "../../src/session/schema"
 import { testEffect } from "../lib/effect"
 
 const it = testEffect(
-  Layer.mergeAll(
-    LSP.defaultLayer,
-    FSUtil.defaultLayer,
-    Format.defaultLayer,
-    EventV2Bridge.defaultLayer,
-    Truncate.defaultLayer,
-    Agent.defaultLayer,
-  ),
+  LayerNode.compile(LayerNode.group([LSP.node, FSUtil.node, Format.node, EventV2Bridge.node, Truncate.node, Agent.node])),
 )
 
 const baseCtx = {

+ 2 - 8
packages/opencode/test/tool/edit.test.ts

@@ -1,6 +1,7 @@
 import { afterEach, describe, expect } from "bun:test"
 import path from "path"
 import fs from "fs/promises"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
 import { EditTool } from "../../src/tool/edit"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
@@ -30,14 +31,7 @@ afterEach(async () => {
   await disposeAllInstances()
 })
 
-const layer = Layer.mergeAll(
-  LSP.defaultLayer,
-  FSUtil.defaultLayer,
-  Format.defaultLayer,
-  EventV2Bridge.defaultLayer,
-  Truncate.defaultLayer,
-  Agent.defaultLayer,
-)
+const layer = LayerNode.compile(LayerNode.group([LSP.node, FSUtil.node, Format.node, EventV2Bridge.node, Truncate.node, Agent.node]))
 
 const it = testEffect(layer)
 

+ 2 - 8
packages/opencode/test/tool/glob.test.ts

@@ -1,6 +1,7 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { describe, expect } from "bun:test"
 import path from "path"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Effect, Exit, Layer } from "effect"
 import { GlobTool } from "../../src/tool/glob"
 import { SessionID, MessageID } from "../../src/session/schema"
@@ -20,14 +21,7 @@ import { Permission } from "../../src/permission"
 import type * as Tool from "../../src/tool/tool"
 
 const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
-  Layer.mergeAll(
-    CrossSpawnSpawner.defaultLayer,
-    FSUtil.defaultLayer,
-    Ripgrep.defaultLayer,
-    Truncate.defaultLayer,
-    Agent.defaultLayer,
-    Git.defaultLayer,
-  )
+  LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node, Ripgrep.node, Truncate.node, Agent.node, Git.node]))
 
 const it = testEffect(toolLayer())
 const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)

+ 2 - 8
packages/opencode/test/tool/grep.test.ts

@@ -3,6 +3,7 @@ import { describe, expect } from "bun:test"
 import fs from "fs/promises"
 import os from "os"
 import path from "path"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import { GrepTool } from "../../src/tool/grep"
 import { provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
@@ -22,14 +23,7 @@ import { Git } from "@/git"
 import { Filesystem } from "@/util/filesystem"
 
 const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
-  Layer.mergeAll(
-    CrossSpawnSpawner.defaultLayer,
-    FSUtil.defaultLayer,
-    Ripgrep.defaultLayer,
-    Truncate.defaultLayer,
-    Agent.defaultLayer,
-    Git.defaultLayer,
-  )
+  LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node, Ripgrep.node, Truncate.node, Agent.node, Git.node]))
 
 const it = testEffect(toolLayer())
 const rooted = testEffect(Layer.mergeAll(toolLayer(), testInstanceStoreLayer))

+ 4 - 1
packages/opencode/test/tool/lsp.test.ts

@@ -1,5 +1,6 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { afterEach, describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import path from "path"
 import { Agent } from "../../src/agent/agent"
@@ -56,7 +57,9 @@ const lsp = Layer.succeed(
 )
 
 const it = testEffect(
-  Layer.mergeAll(Agent.defaultLayer, FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer, Truncate.defaultLayer, lsp),
+  LayerNode.compile(LayerNode.group([Agent.node, FSUtil.node, CrossSpawnSpawner.node, Truncate.node, LSP.node]), [
+    [LSP.node, lsp],
+  ]),
 )
 
 const init = Effect.fn("LspToolTest.init")(function* () {

+ 3 - 8
packages/opencode/test/tool/read.test.ts

@@ -1,5 +1,6 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { afterEach, describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Effect, Exit, Layer, Stream } from "effect"
 import path from "path"
 import { Agent } from "../../src/agent/agent"
@@ -44,14 +45,8 @@ const ctx = {
 }
 
 const readLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
-  Layer.mergeAll(
-    Agent.defaultLayer,
-    FSUtil.defaultLayer,
-    CrossSpawnSpawner.defaultLayer,
-    Instruction.defaultLayer,
-    LSP.defaultLayer,
-    Ripgrep.defaultLayer,
-    Truncate.defaultLayer,
+  LayerNode.compile(
+    LayerNode.group([Agent.node, FSUtil.node, CrossSpawnSpawner.node, Instruction.node, LSP.node, Ripgrep.node, Truncate.node]),
   )
 
 const it = testEffect(Layer.mergeAll(readLayer(), testInstanceStoreLayer))

+ 12 - 7
packages/opencode/test/tool/shell.test.ts

@@ -1,5 +1,6 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Effect, Exit, Layer } from "effect"
 import type * as Scope from "effect/Scope"
 import os from "os"
@@ -22,13 +23,17 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
 import { InstanceStore } from "@/project/instance-store"
 
 const shellLayer = Layer.mergeAll(
-  CrossSpawnSpawner.defaultLayer,
-  FSUtil.defaultLayer,
-  Plugin.defaultLayer,
-  Truncate.defaultLayer,
-  Config.defaultLayer,
-  Agent.defaultLayer,
-  RuntimeFlags.defaultLayer,
+  LayerNode.compile(
+    LayerNode.group([
+      CrossSpawnSpawner.node,
+      FSUtil.node,
+      Plugin.node,
+      Truncate.node,
+      Config.node,
+      Agent.node,
+      RuntimeFlags.node,
+    ]),
+  ),
   testInstanceStoreLayer,
 )
 const it = testEffect(shellLayer)

+ 2 - 3
packages/opencode/test/tool/skill.test.ts

@@ -1,5 +1,6 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Ripgrep } from "@opencode-ai/core/ripgrep"
 import { Cause, Effect, Exit, Layer } from "effect"
 import { afterEach, describe, expect } from "bun:test"
@@ -26,9 +27,7 @@ afterEach(async () => {
   await disposeAllInstances()
 })
 
-const node = CrossSpawnSpawner.defaultLayer
-
-const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node).pipe(Layer.provide(Ripgrep.defaultLayer)))
+const it = testEffect(LayerNode.compile(LayerNode.group([ToolRegistry.node, CrossSpawnSpawner.node, Ripgrep.node])))
 
 describe("tool.skill", () => {
   it.instance("execute returns skill content block with files", () =>

+ 21 - 14
packages/opencode/test/tool/task.test.ts

@@ -1,6 +1,8 @@
 import { afterEach, describe, expect } from "bun:test"
 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 { Deferred, Effect, Exit, Fiber, Layer } from "effect"
 import { Agent } from "../../src/agent/agent"
 import { BackgroundJob } from "@/background/job"
@@ -33,20 +35,25 @@ const ref = {
 }
 
 const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
-  Layer.mergeAll(
-    Agent.defaultLayer,
-    BackgroundJob.defaultLayer,
-    EventV2Bridge.defaultLayer,
-    Config.defaultLayer,
-    CrossSpawnSpawner.defaultLayer,
-    Session.defaultLayer,
-    SessionRunState.defaultLayer,
-    SessionStatus.defaultLayer,
-    Truncate.defaultLayer,
-    ToolRegistry.defaultLayer,
-    Database.defaultLayer,
-    RuntimeFlags.layer(flags),
-  ).pipe(Layer.provide(Ripgrep.defaultLayer))
+  LayerNode.compile(
+    LayerNode.group([
+      Agent.node,
+      BackgroundJob.node,
+      EventV2Bridge.node,
+      Config.node,
+      CrossSpawnSpawner.node,
+      Session.node,
+      SessionProjector.node,
+      SessionRunState.node,
+      SessionStatus.node,
+      Truncate.node,
+      ToolRegistry.node,
+      Database.node,
+      RuntimeFlags.node,
+      Ripgrep.node,
+    ]),
+    [[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
+  )
 
 const it = testEffect(layer())
 const background = testEffect(layer({ experimentalBackgroundSubagents: true }))

+ 8 - 2
packages/opencode/test/tool/webfetch.test.ts

@@ -1,6 +1,8 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
 import { Effect, Layer } from "effect"
-import { FetchHttpClient } from "effect/unstable/http"
+import { FetchHttpClient, HttpClient } from "effect/unstable/http"
 import { Agent } from "../../src/agent/agent"
 import { Truncate } from "@/tool/truncate"
 import { WebFetchTool } from "../../src/tool/webfetch"
@@ -8,7 +10,11 @@ import { SessionID, MessageID } from "../../src/session/schema"
 import { Tool } from "@/tool/tool"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(FetchHttpClient.layer, Truncate.defaultLayer, Agent.defaultLayer))
+const it = testEffect(
+  LayerNode.compile(LayerNode.group([httpClient, Truncate.node, Agent.node]), [
+    [httpClient, FetchHttpClient.layer as Layer.Layer<HttpClient.HttpClient>],
+  ]),
+)
 
 const ctx = {
   sessionID: SessionID.make("ses_test"),

+ 3 - 8
packages/opencode/test/tool/write.test.ts

@@ -1,4 +1,5 @@
 import { afterEach, describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer } from "effect"
 import path from "path"
 import fs from "fs/promises"
@@ -31,14 +32,8 @@ afterEach(async () => {
 })
 
 const it = testEffect(
-  Layer.mergeAll(
-    LSP.defaultLayer,
-    FSUtil.defaultLayer,
-    EventV2Bridge.defaultLayer,
-    Format.defaultLayer,
-    CrossSpawnSpawner.defaultLayer,
-    Truncate.defaultLayer,
-    Agent.defaultLayer,
+  LayerNode.compile(
+    LayerNode.group([LSP.node, FSUtil.node, EventV2Bridge.node, Format.node, CrossSpawnSpawner.node, Truncate.node, Agent.node]),
   ),
 )