Ver código fonte

refactor(core): convert opencode tests to nodes (#34453)

James Long 2 meses atrás
pai
commit
bc5ce5eab1
36 arquivos alterados com 112 adições e 106 exclusões
  1. 3 0
      packages/core/src/observability.ts
  2. 3 0
      packages/opencode/src/acp/session.ts
  3. 4 2
      packages/opencode/test/account/repo.test.ts
  4. 2 1
      packages/opencode/test/acp/event.test.ts
  5. 2 1
      packages/opencode/test/acp/permission.test.ts
  6. 2 1
      packages/opencode/test/acp/session.test.ts
  7. 2 1
      packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts
  8. 3 5
      packages/opencode/test/auth/auth.test.ts
  9. 2 1
      packages/opencode/test/background/job.test.ts
  10. 2 1
      packages/opencode/test/cli/effect-cmd-instance-als.test.ts
  11. 3 3
      packages/opencode/test/config/agent-color.test.ts
  12. 5 3
      packages/opencode/test/effect/app-runtime-logger.test.ts
  13. 3 4
      packages/opencode/test/filesystem/filesystem.test.ts
  14. 7 9
      packages/opencode/test/image/image.test.ts
  15. 3 3
      packages/opencode/test/lsp/lifecycle.test.ts
  16. 2 1
      packages/opencode/test/mcp/headers.test.ts
  17. 2 1
      packages/opencode/test/mcp/lifecycle.test.ts
  18. 2 1
      packages/opencode/test/patch/patch.test.ts
  19. 2 1
      packages/opencode/test/permission-task.test.ts
  20. 2 2
      packages/opencode/test/project/instance-bootstrap.test.ts
  21. 5 2
      packages/opencode/test/project/instance.test.ts
  22. 3 2
      packages/opencode/test/project/migrate-global.test.ts
  23. 3 2
      packages/opencode/test/project/project-directory.test.ts
  24. 3 6
      packages/opencode/test/project/vcs.test.ts
  25. 3 3
      packages/opencode/test/project/worktree-remove.test.ts
  26. 3 5
      packages/opencode/test/project/worktree.test.ts
  27. 3 2
      packages/opencode/test/provider/amazon-bedrock.test.ts
  28. 2 1
      packages/opencode/test/provider/digitalocean.test.ts
  29. 3 4
      packages/opencode/test/provider/header-timeout.test.ts
  30. 4 5
      packages/opencode/test/question/question.test.ts
  31. 6 9
      packages/opencode/test/session/revert-compact.test.ts
  32. 3 2
      packages/opencode/test/skill/discovery.test.ts
  33. 2 1
      packages/opencode/test/tool/external-directory.test.ts
  34. 3 10
      packages/opencode/test/tool/question.test.ts
  35. 3 2
      packages/opencode/test/tool/tool-define.test.ts
  36. 7 9
      packages/opencode/test/tool/truncation.test.ts

+ 3 - 0
packages/core/src/observability.ts

@@ -1,6 +1,7 @@
 export * as Observability from "./observability"
 
 import { NodeFileSystem } from "@effect/platform-node"
+import { LayerNode } from "./effect/layer-node"
 import { Effect, Layer, Logger, References } from "effect"
 import { FetchHttpClient } from "effect/unstable/http"
 import { OtlpSerialization } from "effect/unstable/observability"
@@ -19,3 +20,5 @@ export const layer = Layer.unwrap(
     return Layer.merge(logs, yield* Effect.promise(Otlp.tracingLayer))
   }),
 )
+
+export const node = LayerNode.make({ name: "observability", layer, deps: [] })

+ 3 - 0
packages/opencode/src/acp/session.ts

@@ -1,5 +1,6 @@
 import type { McpServer } from "@agentclientprotocol/sdk"
 import type { Message, Part } from "@opencode-ai/sdk/v2"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { Context, Effect, Layer, Ref } from "effect"
@@ -202,6 +203,8 @@ export const layer = Layer.effect(
 
 export const defaultLayer = layer
 
+export const node = LayerNode.make({ service: Service, layer, deps: [] })
+
 function makeSession(input: StoreInput): Info {
   return {
     id: input.id,

+ 4 - 2
packages/opencode/test/account/repo.test.ts

@@ -1,4 +1,5 @@
 import { expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect, Layer, Option } from "effect"
 import { sql } from "drizzle-orm"
 
@@ -13,9 +14,10 @@ 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])))
 
 it.live("list returns empty when no accounts exist", () =>
   Effect.gen(function* () {

+ 2 - 1
packages/opencode/test/acp/event.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, it } from "bun:test"
 import type { AgentSideConnection } from "@agentclientprotocol/sdk"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import type { Event, Message, OpencodeClient, Part, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
 import { Effect, ManagedRuntime } from "effect"
 import { ACPEvent } from "@/acp/event"
@@ -30,7 +31,7 @@ const pollUntil = async (
 }
 
 function makeSessionService() {
-  return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
+  return ManagedRuntime.make(LayerNode.compile(ACPSession.node)).runSync(
     ACPSession.Service.use((service) => Effect.succeed(service)),
   )
 }

+ 2 - 1
packages/opencode/test/acp/permission.test.ts

@@ -6,6 +6,7 @@ import type {
   SessionUpdate,
 } from "@agentclientprotocol/sdk"
 import type { Event, OpencodeClient } from "@opencode-ai/sdk/v2"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { createTwoFilesPatch } from "diff"
 import { Effect, ManagedRuntime } from "effect"
 import { mkdtemp, rm } from "node:fs/promises"
@@ -37,7 +38,7 @@ const pollUntil = async (
 }
 
 function makeSessionService() {
-  return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
+  return ManagedRuntime.make(LayerNode.compile(ACPSession.node)).runSync(
     ACPSession.Service.use((service) => Effect.succeed(service)),
   )
 }

+ 2 - 1
packages/opencode/test/acp/session.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect } from "bun:test"
 import type { McpServer } from "@agentclientprotocol/sdk"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect } from "effect"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
@@ -7,7 +8,7 @@ import * as ACPError from "@/acp/error"
 import * as ACPSession from "@/acp/session"
 import { testEffect } from "../lib/effect"
 
-const sessionTest = testEffect(ACPSession.defaultLayer)
+const sessionTest = testEffect(LayerNode.compile(ACPSession.node))
 
 const model = (providerID: string, modelID: string): ACPSession.SelectedModel => ({
   providerID: ProviderV2.ID.make(providerID),

+ 2 - 1
packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts

@@ -1,4 +1,5 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { expect } from "bun:test"
 import { Effect } from "effect"
 import { Agent } from "../../src/agent/agent"
@@ -6,7 +7,7 @@ import { deriveSubagentSessionPermission } from "../../src/agent/subagent-permis
 import { Permission } from "../../src/permission"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Agent.defaultLayer)
+const it = testEffect(LayerNode.compile(Agent.node))
 
 function testAgent(input: {
   name: string

+ 3 - 5
packages/opencode/test/auth/auth.test.ts

@@ -1,12 +1,10 @@
 import { describe, expect } from "bun:test"
-import { Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Effect } from "effect"
 import { Auth } from "../../src/auth"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { testEffect } from "../lib/effect"
 
-const node = CrossSpawnSpawner.defaultLayer
-
-const it = testEffect(Layer.mergeAll(Auth.defaultLayer, node))
+const it = testEffect(LayerNode.compile(Auth.node))
 
 describe("Auth", () => {
   it.instance("set normalizes trailing slashes in keys", () =>

+ 2 - 1
packages/opencode/test/background/job.test.ts

@@ -1,9 +1,10 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Deferred, Effect } from "effect"
 import { BackgroundJob } from "@/background/job"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(BackgroundJob.defaultLayer)
+const it = testEffect(LayerNode.compile(BackgroundJob.node))
 
 describe("background.job", () => {
   it.instance("tracks started jobs through completion", () =>

+ 2 - 1
packages/opencode/test/cli/effect-cmd-instance-als.test.ts

@@ -1,4 +1,5 @@
 import { afterEach, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Effect } from "effect"
 import { fileURLToPath } from "url"
@@ -6,7 +7,7 @@ import { InstanceRef } from "../../src/effect/instance-ref"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(FSUtil.defaultLayer)
+const it = testEffect(LayerNode.compile(FSUtil.node))
 
 afterEach(async () => {
   await disposeAllInstances()

+ 3 - 3
packages/opencode/test/config/agent-color.test.ts

@@ -1,11 +1,11 @@
 import { expect } from "bun:test"
-import { Effect, Layer } from "effect"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Effect } from "effect"
 import { Config } from "@/config/config"
 import { Agent as AgentSvc } from "../../src/agent/agent"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Config.defaultLayer, AgentSvc.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Config.node, AgentSvc.node])))
 
 it.instance(
   "agent color parsed from project config",

+ 5 - 3
packages/opencode/test/effect/app-runtime-logger.test.ts

@@ -1,4 +1,5 @@
 import { expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Context, Deferred, Effect, Fiber, Layer, Logger } from "effect"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { AppLayer } from "../../src/effect/app-runtime"
@@ -9,7 +10,8 @@ import { attach } from "../../src/effect/run-service"
 import { TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(CrossSpawnSpawner.defaultLayer)
+const observabilityLayer = LayerNode.compile(Observability.node)
+const it = testEffect(LayerNode.compile(CrossSpawnSpawner.node))
 
 function check(loggers: ReadonlySet<Logger.Logger<unknown, any>>) {
   return {
@@ -34,7 +36,7 @@ it.live("makeRuntime installs the observability logger", () =>
     )
 
     const current = yield* Dummy.use((svc) => svc.current()).pipe(
-      Effect.provide(Layer.provideMerge(layer, Observability.layer)),
+      Effect.provide(Layer.provideMerge(layer, observabilityLayer)),
     )
 
     expect(current.size).toBeGreaterThan(0)
@@ -94,6 +96,6 @@ it.instance(
 
       expect(result.directory).toBe(test.directory)
       expect(result.size).toBeGreaterThan(0)
-    }).pipe(Effect.provide(Observability.layer)),
+    }).pipe(Effect.provide(observabilityLayer)),
   { git: true },
 )

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

@@ -1,12 +1,11 @@
 import { describe, test, expect } from "bun:test"
-import { Effect, Layer } from "effect"
-import { NodeFileSystem } from "@effect/platform-node"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Effect } from "effect"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { testEffect } from "../lib/effect"
 import path from "path"
 
-const live = FSUtil.layer.pipe(Layer.provide(NodeFileSystem.layer))
-const { effect: it } = testEffect(live)
+const { effect: it } = testEffect(LayerNode.compile(FSUtil.node))
 
 describe("FSUtil", () => {
   describe("isDir", () => {

+ 7 - 9
packages/opencode/test/image/image.test.ts

@@ -1,20 +1,18 @@
 import { describe, expect } from "bun:test"
-import { Cause, Effect, Exit, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Cause, Effect, Exit } from "effect"
 import { Image } from "@/image/image"
+import { Config } from "@/config/config"
 import { MessageID, PartID, SessionID } from "@/session/schema"
 import path from "node:path"
 import { TestConfig } from "../fixture/config"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Image.layer.pipe(Layer.provide(TestConfig.layer()))))
+const it = testEffect(LayerNode.compile(Image.node, [[Config.node, TestConfig.layer()]]))
 const tiny = testEffect(
-  Layer.mergeAll(
-    Image.layer.pipe(
-      Layer.provide(
-        TestConfig.layer({ get: () => Effect.succeed({ attachment: { image: { max_base64_bytes: 1 } } }) }),
-      ),
-    ),
-  ),
+  LayerNode.compile(Image.node, [
+    [Config.node, TestConfig.layer({ get: () => Effect.succeed({ attachment: { image: { max_base64_bytes: 1 } } }) })],
+  ]),
 )
 
 function part(mime: string, data: string) {

+ 3 - 3
packages/opencode/test/lsp/lifecycle.test.ts

@@ -1,13 +1,13 @@
 import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
 import path from "path"
-import { Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Effect } from "effect"
 import { LSP } from "@/lsp/lsp"
 import * as LSPServer from "@/lsp/server"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const it = testEffect(LayerNode.compile(LSP.node))
 
 describe("LSP service lifecycle", () => {
   let spawnSpy: ReturnType<typeof spyOn>

+ 2 - 1
packages/opencode/test/mcp/headers.test.ts

@@ -1,4 +1,5 @@
 import { describe, expect, mock, beforeEach } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect } from "effect"
 import { testEffect } from "../lib/effect"
 
@@ -46,7 +47,7 @@ beforeEach(() => {
 
 // Import MCP after mocking
 const { MCP } = await import("../../src/mcp/index")
-const it = testEffect(MCP.defaultLayer)
+const it = testEffect(LayerNode.compile(MCP.node))
 
 describe("mcp.headers", () => {
   it.instance("headers are passed to transports when oauth is enabled (default)", () =>

+ 2 - 1
packages/opencode/test/mcp/lifecycle.test.ts

@@ -2,6 +2,7 @@ import path from "node:path"
 import { pathToFileURL } from "node:url"
 import { expect, mock, beforeEach } from "bun:test"
 import { ListRootsRequestSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Cause, Effect, Exit } from "effect"
 import type { MCP as MCPNS } from "../../src/mcp/index"
 import { testEffect } from "../lib/effect"
@@ -275,7 +276,7 @@ beforeEach(() => {
 const { MCP } = await import("../../src/mcp/index")
 const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
 
-const it = testEffect(MCP.defaultLayer)
+const it = testEffect(LayerNode.compile(MCP.node))
 
 function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server: string) {
   if ("status" in status) return status.status

+ 2 - 1
packages/opencode/test/patch/patch.test.ts

@@ -1,4 +1,5 @@
 import { describe, test, expect, beforeEach, afterEach } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Effect } from "effect"
 import * as fs from "fs/promises"
 import * as path from "path"
@@ -7,7 +8,7 @@ import { Patch } from "../../src/patch"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(FSUtil.defaultLayer)
+const it = testEffect(LayerNode.compile(FSUtil.node))
 
 describe("Patch namespace", () => {
   let tempDir: string

+ 2 - 1
packages/opencode/test/permission-task.test.ts

@@ -1,11 +1,12 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { describe, test, expect } from "bun:test"
 import { Effect } from "effect"
 import { Permission } from "../src/permission"
 import { Config } from "@/config/config"
 import { testEffect } from "./lib/effect"
 
-const it = testEffect(Config.defaultLayer)
+const it = testEffect(LayerNode.compile(Config.node))
 
 const load = Config.use.get()
 

+ 2 - 2
packages/opencode/test/project/instance-bootstrap.test.ts

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

+ 5 - 2
packages/opencode/test/project/instance.test.ts

@@ -1,9 +1,10 @@
 import { describe, expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Deferred, Effect, Fiber, Layer } from "effect"
 import { InstanceRef } from "../../src/effect/instance-ref"
 import { registerDisposer } from "../../src/effect/instance-registry"
-import { InstanceBootstrap } from "../../src/project/bootstrap-service"
+import { InstanceBootstrap } from "../../src/project/bootstrap"
 import { InstanceStore } from "../../src/project/instance-store"
 import { tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
@@ -15,7 +16,9 @@ const noopBootstrap = Layer.succeed(
 )
 
 const it = testEffect(
-  Layer.mergeAll(InstanceStore.defaultLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(noopBootstrap)),
+  LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
+    [InstanceBootstrap.node, noopBootstrap],
+  ]),
 )
 
 const setBootstrap = (run: Effect.Effect<void>) =>

+ 3 - 2
packages/opencode/test/project/migrate-global.test.ts

@@ -9,11 +9,12 @@ import { ProjectV2 } from "@opencode-ai/core/project"
 import { SessionID } from "../../src/session/schema"
 import { $ } from "bun"
 import { tmpdirScoped } from "../fixture/fixture"
-import { Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Effect } from "effect"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Project.defaultLayer, CrossSpawnSpawner.defaultLayer, Database.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Project.node, Database.node, CrossSpawnSpawner.node])))
 
 function legacySessionID() {
   // Global-session migration covers persisted IDs from before prefixed session IDs.

+ 3 - 2
packages/opencode/test/project/project-directory.test.ts

@@ -2,8 +2,9 @@ import { describe, expect } from "bun:test"
 import { $ } from "bun"
 import path from "path"
 import { eq } from "drizzle-orm"
-import { Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Effect } from "effect"
 import { Hash } from "@opencode-ai/core/util/hash"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { Database } from "@opencode-ai/core/database/database"
@@ -13,7 +14,7 @@ import { Project } from "@/project/project"
 import { tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Project.defaultLayer, Database.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Project.node, Database.node, CrossSpawnSpawner.node])))
 
 function directories(projectID: ProjectV2.ID) {
   return Database.Service.use(({ db }) =>

+ 3 - 6
packages/opencode/test/project/vcs.test.ts

@@ -1,8 +1,9 @@
 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 { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { parsePatch } from "diff"
 import { Deferred, Effect, Layer } from "effect"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import fs from "fs/promises"
 import path from "path"
 import {
@@ -24,11 +25,7 @@ import { testEffect } from "../lib/effect"
 
 const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
 
-const layer = Layer.mergeAll(
-  Vcs.layer.pipe(Layer.provideMerge(Git.defaultLayer), Layer.provideMerge(EventV2Bridge.defaultLayer)),
-  CrossSpawnSpawner.defaultLayer,
-  FSUtil.defaultLayer,
-)
+const layer = LayerNode.compile(LayerNode.group([Vcs.node, Git.node, EventV2Bridge.node, FSUtil.node, CrossSpawnSpawner.node]))
 const it = testEffect(layer)
 const worktreeIt = testEffect(Layer.mergeAll(layer, testInstanceStoreLayer))
 

+ 3 - 3
packages/opencode/test/project/worktree-remove.test.ts

@@ -2,13 +2,13 @@ import { $ } from "bun"
 import { describe, expect } from "bun:test"
 import * as fs from "fs/promises"
 import path from "path"
-import { Effect, Layer } from "effect"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Effect } from "effect"
 import { Worktree } from "../../src/worktree"
 import { TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
+const it = testEffect(LayerNode.compile(Worktree.node))
 const wintest = process.platform === "win32" ? it.instance : it.instance.skip
 
 describe("Worktree.remove", () => {

+ 3 - 5
packages/opencode/test/project/worktree.test.ts

@@ -1,17 +1,15 @@
 import { afterEach, describe, expect } from "bun:test"
 import path from "path"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { FSUtil } from "@opencode-ai/core/fs-util"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
-import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
+import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
 import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
 import { Git } from "../../src/git"
 import { Worktree } from "../../src/worktree"
 import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(
-  Layer.mergeAll(Worktree.defaultLayer, FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer),
-)
+const it = testEffect(LayerNode.compile(LayerNode.group([Worktree.node, FSUtil.node, Git.node])))
 const wintest = process.platform !== "win32" ? it.instance : it.instance.skip
 
 function normalize(input: string) {

+ 3 - 2
packages/opencode/test/provider/amazon-bedrock.test.ts

@@ -1,5 +1,6 @@
 import { afterEach, describe, expect, test } from "bun:test"
-import { Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Effect } from "effect"
 import path from "path"
 import { unlink } from "fs/promises"
 import { Global } from "@opencode-ai/core/global"
@@ -12,7 +13,7 @@ import { testEffect } from "../lib/effect"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 
-const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node])))
 
 const originalEnv = new Map<string, string | undefined>()
 

+ 2 - 1
packages/opencode/test/provider/digitalocean.test.ts

@@ -1,4 +1,5 @@
 import { expect } from "bun:test"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { Provider } from "../../src/provider/provider"
 
 import { Effect } from "effect"
@@ -6,7 +7,7 @@ import { testEffect } from "../lib/effect"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 
 const DIGITALOCEAN = ProviderV2.ID.make("digitalocean")
-const it = testEffect(Provider.defaultLayer)
+const it = testEffect(LayerNode.compile(Provider.node))
 
 const withEnv = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>
   Effect.acquireUseRelease(

+ 3 - 4
packages/opencode/test/provider/header-timeout.test.ts

@@ -1,8 +1,9 @@
 import { afterEach, expect } from "bun:test"
 import { createServer, type Server } from "node:http"
 import { streamText } from "ai"
-import { Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Effect } from "effect"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
@@ -17,9 +18,7 @@ afterEach(async () => {
   await disposeAllInstances()
 })
 
-const it = testEffect(
-  Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer, CrossSpawnSpawner.defaultLayer),
-)
+const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node, CrossSpawnSpawner.node])))
 
 it.live("headerTimeout does not abort delayed SSE body after headers arrive", () =>
   Effect.gen(function* () {

+ 4 - 5
packages/opencode/test/question/question.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, Fiber, Layer, Queue } from "effect"
 import { Question } from "../../src/question"
 import { InstanceRef } from "../../src/effect/instance-ref"
@@ -10,13 +11,11 @@ import { testEffect } from "../lib/effect"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { EventV2Bridge } from "../../src/event-v2-bridge"
 
-const it = testEffect(
-  Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer),
-)
+const questionLayer = LayerNode.compile(LayerNode.group([Question.node, EventV2Bridge.node, CrossSpawnSpawner.node]))
+const it = testEffect(questionLayer)
 const lifecycle = testEffect(
   Layer.mergeAll(
-    Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)),
-    CrossSpawnSpawner.defaultLayer,
+    questionLayer,
     testInstanceStoreLayer,
   ),
 )

+ 6 - 9
packages/opencode/test/session/revert-compact.test.ts

@@ -1,29 +1,26 @@
 import { describe, expect } from "bun:test"
 import { SessionV1 } from "@opencode-ai/core/v1/session"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { SessionProjector } from "@opencode-ai/core/session/projector"
 import fs from "fs/promises"
 import path from "path"
-import { Effect, Layer } from "effect"
+import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { Effect } from "effect"
 import { Session } from "@/session/session"
 
 import { SessionRevert } from "../../src/session/revert"
 import { MessageV2 } from "../../src/session/message-v2"
 import { Snapshot } from "../../src/snapshot"
 import { MessageID, PartID, SessionID } from "../../src/session/schema"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { provideTmpdirInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 
-const env = Layer.mergeAll(
-  Session.defaultLayer,
-  SessionRevert.defaultLayer,
-  Snapshot.defaultLayer,
-  CrossSpawnSpawner.defaultLayer,
+const it = testEffect(
+  LayerNode.compile(LayerNode.group([Session.node, SessionRevert.node, Snapshot.node, SessionProjector.node, CrossSpawnSpawner.node])),
 )
 
-const it = testEffect(env)
-
 const user = Effect.fn("test.user")(function* (sessionID: SessionID, agent = "default") {
   const session = yield* Session.Service
   return yield* session.updateMessage({

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

@@ -1,6 +1,7 @@
 import { describe, expect, beforeAll, afterAll } 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 { Effect } from "effect"
 import { Discovery } from "../../src/skill/discovery"
 import { Global } from "@opencode-ai/core/global"
 import { Filesystem } from "@/util/filesystem"
@@ -18,7 +19,7 @@ let mutableFiles = ["SKILL.md"]
 
 const fixturePath = path.join(import.meta.dir, "../fixture/skills")
 const cacheDir = path.join(Global.Path.cache, "skills")
-const it = testEffect(Layer.mergeAll(Discovery.defaultLayer, FSUtil.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Discovery.node, FSUtil.node])))
 
 beforeAll(async () => {
   await rm(cacheDir, { recursive: true, force: true })

+ 2 - 1
packages/opencode/test/tool/external-directory.test.ts

@@ -1,4 +1,5 @@
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { describe, expect } from "bun:test"
 import path from "path"
 import { Effect } from "effect"
@@ -11,7 +12,7 @@ import type { Permission } from "../../src/permission"
 import { SessionID, MessageID } from "../../src/session/schema"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(CrossSpawnSpawner.defaultLayer)
+const it = testEffect(LayerNode.compile(CrossSpawnSpawner.node))
 
 const baseCtx: Omit<Tool.Context, "ask"> = {
   sessionID: SessionID.make("ses_test"),

+ 3 - 10
packages/opencode/test/tool/question.test.ts

@@ -1,10 +1,10 @@
 import { describe, expect } from "bun:test"
-import { Effect, Fiber, Layer, Queue } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Effect, Fiber, Queue } from "effect"
 import { QuestionTool } from "../../src/tool/question"
 import { Question } from "../../src/question"
 import { SessionID, MessageID } from "../../src/session/schema"
 import { Agent } from "../../src/agent/agent"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Truncate } from "@/tool/truncate"
 import { testEffect } from "../lib/effect"
 import { EventV2Bridge } from "../../src/event-v2-bridge"
@@ -20,14 +20,7 @@ const ctx = {
   ask: () => Effect.void,
 }
 
-const it = testEffect(
-  Layer.mergeAll(
-    Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)),
-    CrossSpawnSpawner.defaultLayer,
-    Truncate.defaultLayer,
-    Agent.defaultLayer,
-  ),
-)
+const it = testEffect(LayerNode.compile(LayerNode.group([Question.node, EventV2Bridge.node, Truncate.node, Agent.node])))
 
 const pending = Effect.fn("QuestionToolTest.pending")(function* (question: Question.Interface) {
   const events = yield* EventV2Bridge.Service

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

@@ -1,12 +1,13 @@
 import { describe, expect } from "bun:test"
-import { Cause, Effect, Exit, Layer, Schema } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Cause, Effect, Exit, Schema } from "effect"
 import { Agent } from "../../src/agent/agent"
 import { MessageID, SessionID } from "../../src/session/schema"
 import { Tool } from "@/tool/tool"
 import { Truncate } from "@/tool/truncate"
 import { testEffect } from "../lib/effect"
 
-const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Truncate.node, Agent.node])))
 
 const params = Schema.Struct({ input: Schema.String })
 

+ 7 - 9
packages/opencode/test/tool/truncation.test.ts

@@ -1,8 +1,9 @@
 import { describe, test, expect } from "bun:test"
 import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
-import { NodeFileSystem } from "@effect/platform-node"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { filesystem } from "@opencode-ai/core/effect/app-node-platform"
 import { FSUtil } from "@opencode-ai/core/fs-util"
-import { Effect, FileSystem, Layer } from "effect"
+import { Effect, FileSystem } from "effect"
 import { Truncate } from "@/tool/truncate"
 import { Config } from "@/config/config"
 import { Identifier } from "../../src/id/id"
@@ -15,15 +16,12 @@ import { TestConfig } from "../fixture/config"
 const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
 const ROOT = path.resolve(import.meta.dir, "..", "..")
 
-const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, NodeFileSystem.layer, FSUtil.defaultLayer))
+const it = testEffect(LayerNode.compile(LayerNode.group([Truncate.node, FSUtil.node, filesystem])))
 
 const configuredLayer = (cfg: ConfigV1.Info) =>
-  Layer.mergeAll(
-    Truncate.defaultLayer,
-    NodeFileSystem.layer,
-    FSUtil.defaultLayer,
-    TestConfig.layer({ get: () => Effect.succeed(cfg) }),
-  )
+  LayerNode.compile(LayerNode.group([Truncate.node, FSUtil.node, filesystem, Config.node]), [
+    [Config.node, TestConfig.layer({ get: () => Effect.succeed(cfg) })],
+  ])
 const configuredIt = (cfg: ConfigV1.Info) => testEffect(configuredLayer(cfg))
 
 describe("Truncate", () => {