From b9fa63a0bc710d63987d984d73ea0f502fd306e8 Mon Sep 17 00:00:00 2001 From: James Long Date: Sun, 28 Jun 2026 23:53:07 -0400 Subject: [PATCH 1/2] refactor(core): convert more tests to layer nodes --- packages/core/test/agent.test.ts | 3 +- packages/core/test/background-job.test.ts | 9 ++- packages/core/test/command.test.ts | 3 +- packages/core/test/location.test.ts | 3 +- packages/core/test/npm.test.ts | 13 +--- packages/core/test/plugin/command.test.ts | 8 +-- packages/core/test/policy.test.ts | 9 ++- packages/core/test/pty/pty-session.test.ts | 23 ++++---- packages/core/test/pty/ticket.test.ts | 7 ++- packages/core/test/reference-guidance.test.ts | 59 ++++++++++--------- packages/core/test/session-create.test.ts | 33 ++++------- packages/core/test/session-history.test.ts | 24 ++------ packages/core/test/session-prompt.test.ts | 22 ++----- .../test/session-runner-tool-registry.test.ts | 10 +++- packages/core/test/skill/guidance.test.ts | 5 +- .../core/test/system-context/registry.test.ts | 3 +- packages/core/test/tool-question.test.ts | 18 ++++-- packages/core/test/tool-todowrite.test.ts | 23 +++++--- packages/core/test/tool-webfetch.test.ts | 29 +++++---- packages/core/test/tool-websearch.test.ts | 28 ++++++--- 20 files changed, 174 insertions(+), 158 deletions(-) diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index ef8d0747d8..f4c023e5d5 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -1,6 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Exit, Scope } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { AgentPlugin } from "@opencode-ai/core/plugin/agent" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -8,7 +9,7 @@ import { location } from "./fixture/location" import { testEffect } from "./lib/effect" import { agentHost, host } from "./plugin/host" -const it = testEffect(AgentV2.locationLayer) +const it = testEffect(AppNodeBuilder.build(AgentV2.node)) describe("AgentV2", () => { it.effect("starts without agents", () => diff --git a/packages/core/test/background-job.test.ts b/packages/core/test/background-job.test.ts index 1c4f93f019..5ad1e061a5 100644 --- a/packages/core/test/background-job.test.ts +++ b/packages/core/test/background-job.test.ts @@ -1,8 +1,11 @@ import { describe, expect } from "bun:test" import { BackgroundJob } from "@opencode-ai/core/background-job" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Deferred, Effect, Exit, Scope } from "effect" import { it } from "./lib/effect" +const jobsLayer = LayerNode.compile(BackgroundJob.node) + describe("BackgroundJob", () => { it.live("tracks process-local work through explicit observation", () => Effect.gen(function* () { @@ -25,7 +28,7 @@ describe("BackgroundJob", () => { timedOut: false, info: { status: "completed", output: "done" }, }) - }).pipe(Effect.provide(BackgroundJob.layer)), + }).pipe(Effect.provide(jobsLayer)), ) it.live("publishes jobs before starting immediately settling work", () => @@ -55,7 +58,7 @@ describe("BackgroundJob", () => { }) }) }) - }).pipe(Effect.provide(BackgroundJob.layer)), + }).pipe(Effect.provide(jobsLayer)), ) it.live("increments pending work before starting immediately settling extensions", () => @@ -80,7 +83,7 @@ describe("BackgroundJob", () => { }) }), ) - }).pipe(Effect.provide(BackgroundJob.layer)), + }).pipe(Effect.provide(jobsLayer)), ) it.live("interrupts live work without promising settlement after the owning process-local scope closes", () => diff --git a/packages/core/test/command.test.ts b/packages/core/test/command.test.ts index 814da3a787..3da8523c5b 100644 --- a/packages/core/test/command.test.ts +++ b/packages/core/test/command.test.ts @@ -1,11 +1,12 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { CommandV2 } from "@opencode-ai/core/command" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" import { testEffect } from "./lib/effect" -const it = testEffect(CommandV2.locationLayer) +const it = testEffect(AppNodeBuilder.build(CommandV2.node)) describe("CommandV2", () => { it.effect("applies command transforms and preserves later overrides", () => diff --git a/packages/core/test/location.test.ts b/packages/core/test/location.test.ts index 327c5bff9f..77e9e96745 100644 --- a/packages/core/test/location.test.ts +++ b/packages/core/test/location.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -21,7 +22,7 @@ const projectLayer = Layer.succeed( commit: () => Effect.void, }), ) -const it = testEffect(Location.layer(ref).pipe(Layer.provide(projectLayer))) +const it = testEffect(AppNodeBuilder.build(Location.boundNode(ref), [[Project.node, projectLayer]])) describe("Location", () => { it.effect("resolves the current project and vcs information", () => diff --git a/packages/core/test/npm.test.ts b/packages/core/test/npm.test.ts index 349d9ab7e4..7e4a5763bf 100644 --- a/packages/core/test/npm.test.ts +++ b/packages/core/test/npm.test.ts @@ -1,12 +1,10 @@ import fs from "fs/promises" import path from "path" import { describe, expect, test } from "bun:test" -import { NodeFileSystem } from "@effect/platform-node" -import { Effect, Layer, Option } from "effect" -import { FSUtil } from "@opencode-ai/core/fs-util" +import { Effect, Option } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Global } from "@opencode-ai/core/global" import { Npm } from "@opencode-ai/core/npm" -import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { tmpdir } from "./fixture/tmpdir" const win = process.platform === "win32" @@ -21,12 +19,7 @@ const writePackage = (dir: string, pkg: Record) => ) const npmLayer = (cache: string) => - Npm.layer.pipe( - Layer.provide(EffectFlock.layer), - Layer.provide(FSUtil.layer), - Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })), - Layer.provide(NodeFileSystem.layer), - ) + AppNodeBuilder.build(Npm.node, [[Global.node, Global.layerWith({ cache, state: path.join(cache, "state") })]]) describe("Npm.sanitize", () => { test("keeps normal scoped package specs unchanged", () => { diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index d4e2500c21..4a49fe48db 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -1,6 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { CommandV2 } from "@opencode-ai/core/command" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { CommandPlugin } from "@opencode-ai/core/plugin/command" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -10,12 +11,9 @@ import { host } from "./host" const directory = AbsolutePath.make("/repo/packages/app") const project = AbsolutePath.make("/repo") +const locationLayer = Layer.succeed(Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project }))) const it = testEffect( - CommandV2.locationLayer.pipe( - Layer.provide( - Layer.succeed(Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project }))), - ), - ), + AppNodeBuilder.build(CommandV2.node, [[Location.node, locationLayer]]), ) describe("CommandPlugin.Plugin", () => { diff --git a/packages/core/test/policy.test.ts b/packages/core/test/policy.test.ts index 42736eb7d8..9b5309c6d6 100644 --- a/packages/core/test/policy.test.ts +++ b/packages/core/test/policy.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { Policy } from "@opencode-ai/core/policy" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -7,11 +8,9 @@ import { location } from "./fixture/location" import { testEffect } from "./lib/effect" const it = testEffect( - Policy.locationLayer.pipe( - Layer.provide( - Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), - ), - ), + AppNodeBuilder.build(Policy.node, [ + [Location.node, Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") })))], + ]), ) describe("Policy", () => { diff --git a/packages/core/test/pty/pty-session.test.ts b/packages/core/test/pty/pty-session.test.ts index ab15e16c48..6d049b49ae 100644 --- a/packages/core/test/pty/pty-session.test.ts +++ b/packages/core/test/pty/pty-session.test.ts @@ -1,6 +1,8 @@ import { describe, expect } from "bun:test" import { Cause, Deferred, Effect, Exit, Layer, Queue } from "effect" import { Config } from "@opencode-ai/core/config" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" import { Location } from "@opencode-ai/core/location" import { Pty } from "@opencode-ai/core/pty" @@ -17,11 +19,10 @@ const locationLayer = Layer.succeed( ) const configLayer = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) }) const it = testEffect( - Pty.layer.pipe( - Layer.provide(configLayer), - Layer.provideMerge(EventV2.defaultLayer), - Layer.provideMerge(locationLayer), - ), + AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [ + [Config.node, configLayer], + [Location.node, locationLayer], + ]), ) const ptyTest = process.platform === "win32" ? it.live.skip : it.live @@ -205,8 +206,9 @@ describe("pty", () => { const configuredShell = process.platform === "win32" ? undefined : Bun.which("bash") const configuredIt = testEffect( - Pty.layer.pipe( - Layer.provide( + AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [ + [ + Config.node, Layer.mock(Config.Service)({ entries: () => Effect.succeed( @@ -215,10 +217,9 @@ const configuredIt = testEffect( : [], ), }), - ), - Layer.provideMerge(EventV2.defaultLayer), - Layer.provideMerge(locationLayer), - ), + ], + [Location.node, locationLayer], + ]), ) const configuredTest = process.platform === "win32" ? configuredIt.live.skip : configuredIt.live diff --git a/packages/core/test/pty/ticket.test.ts b/packages/core/test/pty/ticket.test.ts index e36808fa2d..10849f2f61 100644 --- a/packages/core/test/pty/ticket.test.ts +++ b/packages/core/test/pty/ticket.test.ts @@ -1,12 +1,15 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { PtyID } from "@opencode-ai/core/pty/schema" import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { testEffect } from "../lib/effect" -const it = testEffect(PtyTicket.layer) -const itExpiring = testEffect(Layer.effect(PtyTicket.Service, PtyTicket.make(5))) +const it = testEffect(LayerNode.compile(PtyTicket.node)) +const itExpiring = testEffect( + LayerNode.compile(PtyTicket.node, [[PtyTicket.node, Layer.effect(PtyTicket.Service, PtyTicket.make(5))]]), +) describe("PTY websocket tickets", () => { it.live("consumes tickets once", () => diff --git a/packages/core/test/reference-guidance.test.ts b/packages/core/test/reference-guidance.test.ts index 6f11cbc431..2a317af2ca 100644 --- a/packages/core/test/reference-guidance.test.ts +++ b/packages/core/test/reference-guidance.test.ts @@ -1,11 +1,15 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AbsolutePath } from "@opencode-ai/core/schema" import { Reference } from "@opencode-ai/core/reference" import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" import { SystemContext } from "@opencode-ai/core/system-context/index" import { it } from "./lib/effect" +const guidanceLayer = (referenceLayer: Layer.Layer) => + AppNodeBuilder.build(ReferenceGuidance.node, [[Reference.node, referenceLayer]]) + describe("ReferenceGuidance", () => { it.effect("lists available references in the system context", () => Effect.gen(function* () { @@ -17,23 +21,24 @@ describe("ReferenceGuidance", () => { expect(generation.baseline).toContain("/docs") expect(generation.baseline).toContain("Use for product documentation") }).pipe( - Effect.provide(ReferenceGuidance.layer), Effect.provide( - Layer.mock(Reference.Service, { - list: () => - Effect.succeed([ - new Reference.Info({ - name: "docs", - path: AbsolutePath.make("/docs"), - description: "Use for product documentation", - source: Reference.LocalSource.make({ - type: "local", + guidanceLayer( + Layer.mock(Reference.Service, { + list: () => + Effect.succeed([ + new Reference.Info({ + name: "docs", path: AbsolutePath.make("/docs"), description: "Use for product documentation", + source: Reference.LocalSource.make({ + type: "local", + path: AbsolutePath.make("/docs"), + description: "Use for product documentation", + }), }), - }), - ]), - }), + ]), + }), + ), ), ), ) @@ -43,10 +48,7 @@ describe("ReferenceGuidance", () => { const guidance = yield* ReferenceGuidance.Service const generation = yield* SystemContext.initialize(yield* guidance.load()) expect(generation.baseline).toBe("") - }).pipe( - Effect.provide(ReferenceGuidance.layer), - Effect.provide(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })), - ), + }).pipe(Effect.provide(guidanceLayer(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })))), ) it.effect("omits references without descriptions", () => @@ -55,18 +57,19 @@ describe("ReferenceGuidance", () => { const generation = yield* SystemContext.initialize(yield* guidance.load()) expect(generation.baseline).toBe("") }).pipe( - Effect.provide(ReferenceGuidance.layer), Effect.provide( - Layer.mock(Reference.Service, { - list: () => - Effect.succeed([ - new Reference.Info({ - name: "docs", - path: AbsolutePath.make("/docs"), - source: Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") }), - }), - ]), - }), + guidanceLayer( + Layer.mock(Reference.Service, { + list: () => + Effect.succeed([ + new Reference.Info({ + name: "docs", + path: AbsolutePath.make("/docs"), + source: Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") }), + }), + ]), + }), + ), ), ), ) diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 81efeb7623..54ff364901 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -4,6 +4,8 @@ import { Effect, Layer, Stream } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { asc, eq } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" import { EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" @@ -13,7 +15,6 @@ import { ProjectTable } from "@opencode-ai/core/project/sql" import { ProviderV2 } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" -import { locationServiceMapLayer } from "@opencode-ai/core/location-services" import { SessionV1 } from "@opencode-ai/core/v1/session" import { Prompt } from "@opencode-ai/core/session/prompt" import { SessionProjector } from "@opencode-ai/core/session/projector" @@ -34,24 +35,11 @@ const projects = Layer.succeed( commit: () => Effect.void, }), ) -const sessions = SessionV2.layer.pipe( - Layer.provide(locationServiceMapLayer), - Layer.provide(EventV2.defaultLayer), - Layer.provide(Database.defaultLayer), - Layer.provide(SessionStore.defaultLayer), - Layer.provide(projects), - Layer.provide(SessionExecution.noopLayer), -) const it = testEffect( - Layer.mergeAll( - Database.defaultLayer, - EventV2.defaultLayer, - projects, - SessionProjector.defaultLayer, - SessionStore.defaultLayer, - SessionExecution.noopLayer, - sessions, - ), + AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), [ + [ProjectV2.node, projects], + [SessionExecution.node, SessionExecution.noopLayer], + ]), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) const id = SessionV2.ID.create() @@ -248,9 +236,10 @@ describe("SessionV2.create", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) const targetDatabase = Database.layerFromPath(path.join(tmp.path, "target.sqlite")) - const targetEvents = EventV2.layer.pipe(Layer.provide(targetDatabase)) - const targetProjector = SessionProjector.layer.pipe(Layer.provide(targetEvents), Layer.provide(targetDatabase)) - const targetStore = SessionStore.layer.pipe(Layer.provide(targetDatabase)) + const targetLayer = AppNodeBuilder.build( + LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node]), + [[Database.node, targetDatabase]], + ) yield* Effect.gen(function* () { const db = (yield* Database.Service).db @@ -298,7 +287,7 @@ describe("SessionV2.create", () => { [1, EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1)], [2, EventV2.versionedType(SessionEvent.Prompted.type, 1)], ]) - }).pipe(Effect.provide(Layer.fresh(Layer.mergeAll(targetDatabase, targetEvents, targetProjector, targetStore)))) + }).pipe(Effect.provide(Layer.fresh(targetLayer))) }), ) diff --git a/packages/core/test/session-history.test.ts b/packages/core/test/session-history.test.ts index 9fbfb786d3..e5cb7eccc4 100644 --- a/packages/core/test/session-history.test.ts +++ b/packages/core/test/session-history.test.ts @@ -1,9 +1,10 @@ import { describe, expect } from "bun:test" import { Effect, Layer, Schema } from "effect" import { Database } from "@opencode-ai/core/database/database" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" import { Location } from "@opencode-ai/core/location" -import { locationServiceMapLayer } from "@opencode-ai/core/location-services" import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -22,24 +23,11 @@ const projects = Layer.succeed( commit: () => Effect.void, }), ) -const sessions = SessionV2.layer.pipe( - Layer.provide(locationServiceMapLayer), - Layer.provide(EventV2.defaultLayer), - Layer.provide(Database.defaultLayer), - Layer.provide(SessionStore.defaultLayer), - Layer.provide(projects), - Layer.provide(SessionExecution.noopLayer), -) const it = testEffect( - Layer.mergeAll( - Database.defaultLayer, - EventV2.defaultLayer, - projects, - SessionProjector.defaultLayer, - SessionStore.defaultLayer, - SessionExecution.noopLayer, - sessions, - ), + AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), [ + [ProjectV2.node, projects], + [SessionExecution.node, SessionExecution.noopLayer], + ]), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 66c8f76914..7c0c824b71 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -2,6 +2,8 @@ import { describe, expect } from "bun:test" import { DateTime, Effect, Fiber, Layer, Stream } from "effect" import { eq } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" import { EventTable } from "@opencode-ai/core/event/sql" import { SessionEvent } from "@opencode-ai/core/session/event" @@ -9,7 +11,6 @@ import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" -import { locationServiceMapLayer } from "@opencode-ai/core/location-services" import { Prompt } from "@opencode-ai/core/session/prompt" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" @@ -41,23 +42,10 @@ const execution = Layer.succeed( }), }), ) -const sessions = SessionV2.layer.pipe( - Layer.provide(locationServiceMapLayer), - Layer.provide(EventV2.defaultLayer), - Layer.provide(Database.defaultLayer), - Layer.provide(SessionStore.defaultLayer), - Layer.provide(Project.defaultLayer), - Layer.provide(execution), -) const it = testEffect( - Layer.mergeAll( - Database.defaultLayer, - EventV2.defaultLayer, - SessionProjector.defaultLayer, - SessionStore.defaultLayer, - execution, - sessions, - ), + AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), [ + [SessionExecution.node, execution], + ]), ) const sessionID = SessionV2.ID.make("ses_prompt_test") const messageID = SessionMessage.ID.create() diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index d286c29285..68597f8452 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -1,6 +1,8 @@ import { describe, expect } from "bun:test" import { Tool } from "@opencode-ai/core/tool/tool" import { AgentV2 } from "@opencode-ai/core/agent" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { ApplicationTools } from "@opencode-ai/core/tool/application-tools" import { SessionV2 } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" @@ -27,9 +29,11 @@ const outputStore = Layer.mock(ToolOutputStore.Service, { ) }, }) -const registry = ToolRegistry.layer.pipe(Layer.provide(ApplicationTools.layer), Layer.provide(outputStore)) -const it = testEffect(registry) -const integrated = testEffect(Layer.mergeAll(ApplicationTools.layer, registry)) +const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]]) +const it = testEffect(registryLayer) +const integrated = testEffect( + AppNodeBuilder.build(LayerNode.group([ApplicationTools.node, ToolRegistry.node]), [[ToolOutputStore.node, outputStore]]), +) const identity = { agent: AgentV2.ID.make("build"), assistantMessageID: SessionMessage.ID.make("msg_registry"), diff --git a/packages/core/test/skill/guidance.test.ts b/packages/core/test/skill/guidance.test.ts index 4ebd0e4574..b059201b30 100644 --- a/packages/core/test/skill/guidance.test.ts +++ b/packages/core/test/skill/guidance.test.ts @@ -2,6 +2,7 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AbsolutePath } from "@opencode-ai/core/schema" import { SkillV2 } from "@opencode-ai/core/skill" import { SystemContext } from "@opencode-ai/core/system-context" @@ -28,7 +29,9 @@ const denied = SkillV2.Info.make({ }) const layer = (list: () => SkillV2.Info[]) => - SkillGuidance.layer.pipe(Layer.provide(Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) }))) + AppNodeBuilder.build(SkillGuidance.node, [ + [SkillV2.node, Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) })], + ]) describe("SkillGuidance", () => { it.effect("renders described agent skills and reconciles the complete available list", () => { diff --git a/packages/core/test/system-context/registry.test.ts b/packages/core/test/system-context/registry.test.ts index b77b4132a4..3e68493078 100644 --- a/packages/core/test/system-context/registry.test.ts +++ b/packages/core/test/system-context/registry.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" import { Cause, Effect, Exit, Schema, Scope } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { SystemContext } from "@opencode-ai/core/system-context" import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" import { testEffect } from "../lib/effect" @@ -17,7 +18,7 @@ const entry = (key: string, text: string, sourceKey = key) => ({ ), }) -const it = testEffect(SystemContextRegistry.layer) +const it = testEffect(AppNodeBuilder.build(SystemContextRegistry.node)) describe("SystemContextRegistry", () => { it.effect("loads empty system context when there are no entries", () => diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index 1df2c8331b..8bd36fbf73 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Exit, Fiber, Layer } from "effect" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { PermissionV2 } from "@opencode-ai/core/permission" @@ -31,9 +32,6 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) -const registry = AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode]), [ - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], -]) const question = Layer.succeed( QuestionV2.Service, QuestionV2.Service.of({ @@ -46,8 +44,18 @@ const question = Layer.succeed( list: () => Effect.die("unused"), }), ) -const tool = QuestionTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(question)) -const it = testEffect(Layer.mergeAll(permission, registry, question, tool)) +const toolNode = makeLocationNode({ + name: "test/question-tool", + layer: QuestionTool.layer, + deps: [ToolRegistry.node, PermissionV2.node, QuestionV2.node], +}) +const it = testEffect( + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, toolNode]), [ + [PermissionV2.node, permission], + [QuestionV2.node, question], + [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + ]), +) describe("QuestionTool", () => { it.effect("omits a denied built-in question and terminally settles a stale call", () => diff --git a/packages/core/test/tool-todowrite.test.ts b/packages/core/test/tool-todowrite.test.ts index 174d6139c8..51cbe509d3 100644 --- a/packages/core/test/tool-todowrite.test.ts +++ b/packages/core/test/tool-todowrite.test.ts @@ -1,6 +1,9 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Database } from "@opencode-ai/core/database/database" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" import { PermissionV2 } from "@opencode-ai/core/permission" import { Project } from "@opencode-ai/core/project" @@ -11,6 +14,7 @@ import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionTodo } from "@opencode-ai/core/session/todo" import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite" import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { testEffect } from "./lib/effect" import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" @@ -32,14 +36,19 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) -const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission)) -const tool = TodoWriteTool.layer.pipe( - Layer.provide(registry), - Layer.provide(permission), - Layer.provide(SessionTodo.defaultLayer), -) +const toolNode = makeLocationNode({ + name: "test/todowrite-tool", + layer: TodoWriteTool.layer, + deps: [ToolRegistry.node, PermissionV2.node, SessionTodo.node], +}) const it = testEffect( - Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionTodo.defaultLayer, permission, registry, tool), + AppNodeBuilder.build( + LayerNode.group([Database.node, EventV2.node, SessionTodo.node, ToolRegistry.node, ToolRegistry.toolsNode, toolNode]), + [ + [PermissionV2.node, permission], + [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + ], + ), ) const setup = Effect.gen(function* () { diff --git a/packages/core/test/tool-webfetch.test.ts b/packages/core/test/tool-webfetch.test.ts index 5a856ffaf4..0077d9fb5b 100644 --- a/packages/core/test/tool-webfetch.test.ts +++ b/packages/core/test/tool-webfetch.test.ts @@ -1,11 +1,16 @@ import { describe, expect, test } from "bun:test" import { Duration, Effect, Fiber, Layer, Schema } from "effect" import * as TestClock from "effect/testing/TestClock" -import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" +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" import { PermissionV2 } from "@opencode-ai/core/permission" import { SessionV2 } from "@opencode-ai/core/session" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { WebFetchTool } from "@opencode-ai/core/tool/webfetch" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { testEffect } from "./lib/effect" import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" @@ -35,15 +40,19 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) -const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission)) -const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(http)) -const it = testEffect(Layer.mergeAll(registry, permission, http, webfetch)) -const fetchWebfetch = WebFetchTool.layer.pipe( - Layer.provide(registry), - Layer.provide(permission), - Layer.provide(FetchHttpClient.layer), -) -const live = testEffect(Layer.mergeAll(registry, permission, FetchHttpClient.layer, fetchWebfetch)) +const toolNode = makeLocationNode({ + name: "test/webfetch-tool", + layer: WebFetchTool.layer, + deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient], +}) +const toolLayer = (replacements: LayerNode.Replacements = []) => + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, toolNode]), [ + [PermissionV2.node, permission], + [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + ...replacements, + ]) +const it = testEffect(toolLayer([[LayerNodePlatform.httpClient, http]])) +const live = testEffect(toolLayer()) const reset = () => { requests.length = 0 diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 9715dd5c7f..4bae9ff51a 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -1,10 +1,15 @@ import { beforeEach, describe, expect, test } from "bun:test" import { Effect, Layer, Schema } from "effect" import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" +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" import { PermissionV2 } from "@opencode-ai/core/permission" import { SessionV2 } from "@opencode-ai/core/session" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { WebSearchTool } from "@opencode-ai/core/tool/websearch" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { testEffect } from "./lib/effect" import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" @@ -99,7 +104,6 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) -const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission)) const websearchConfig = Layer.succeed( WebSearchTool.ConfigService, WebSearchTool.ConfigService.of({ @@ -120,13 +124,23 @@ const websearchConfig = Layer.succeed( }, }), ) -const websearch = WebSearchTool.layer.pipe( - Layer.provide(registry), - Layer.provide(permission), - Layer.provide(http), - Layer.provide(websearchConfig), +const websearchConfigNode = makeLocationNode({ + service: WebSearchTool.ConfigService, + layer: websearchConfig, + deps: [], +}) +const toolNode = makeLocationNode({ + name: "test/websearch-tool", + layer: WebSearchTool.layer, + deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient, websearchConfigNode], +}) +const it = testEffect( + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, websearchConfigNode, toolNode]), [ + [PermissionV2.node, permission], + [LayerNodePlatform.httpClient, http], + [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + ]), ) -const it = testEffect(Layer.mergeAll(registry, permission, http, websearchConfig, websearch)) describe("WebSearchTool registration", () => { it.effect("registers websearch, asserts query permission, and calls Exa", () => From 4c1c374a41ed0951e8ef4765e65385bff0ba89df Mon Sep 17 00:00:00 2001 From: James Long Date: Sun, 28 Jun 2026 23:54:35 -0400 Subject: [PATCH 2/2] test(core): fix layer node type assertion --- packages/core/test/effect/layer-node/layer-node-types.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/effect/layer-node/layer-node-types.test.ts b/packages/core/test/effect/layer-node/layer-node-types.test.ts index 0f3dd96d0a..366bf746da 100644 --- a/packages/core/test/effect/layer-node/layer-node-types.test.ts +++ b/packages/core/test/effect/layer-node/layer-node-types.test.ts @@ -69,8 +69,8 @@ void invalidNodeReplacement // @ts-expect-error Replacement cannot introduce a new error LayerNode.compile(a, [[a, Layer.effect(A, Effect.fail(new OtherError()))]]) -// @ts-expect-error Node replacement cannot introduce a new error const invalidNodeErrorReplacement = () => + // @ts-expect-error Node replacement cannot introduce a new error LayerNode.compile(a, [[a, make({ service: A, layer: Layer.effect(A, Effect.fail(new OtherError())), deps: [] })]]) void invalidNodeErrorReplacement