Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
James Long
4c1c374a41 test(core): fix layer node type assertion 2026-06-28 23:54:35 -04:00
James Long
b9fa63a0bc refactor(core): convert more tests to layer nodes 2026-06-28 23:53:07 -04:00
21 changed files with 175 additions and 159 deletions

View file

@ -1,6 +1,7 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Exit, Scope } from "effect" import { Effect, Exit, Scope } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent" 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 { Location } from "@opencode-ai/core/location"
import { AgentPlugin } from "@opencode-ai/core/plugin/agent" import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
@ -8,7 +9,7 @@ import { location } from "./fixture/location"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { agentHost, host } from "./plugin/host" import { agentHost, host } from "./plugin/host"
const it = testEffect(AgentV2.locationLayer) const it = testEffect(AppNodeBuilder.build(AgentV2.node))
describe("AgentV2", () => { describe("AgentV2", () => {
it.effect("starts without agents", () => it.effect("starts without agents", () =>

View file

@ -1,8 +1,11 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { BackgroundJob } from "@opencode-ai/core/background-job" 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 { Deferred, Effect, Exit, Scope } from "effect"
import { it } from "./lib/effect" import { it } from "./lib/effect"
const jobsLayer = LayerNode.compile(BackgroundJob.node)
describe("BackgroundJob", () => { describe("BackgroundJob", () => {
it.live("tracks process-local work through explicit observation", () => it.live("tracks process-local work through explicit observation", () =>
Effect.gen(function* () { Effect.gen(function* () {
@ -25,7 +28,7 @@ describe("BackgroundJob", () => {
timedOut: false, timedOut: false,
info: { status: "completed", output: "done" }, info: { status: "completed", output: "done" },
}) })
}).pipe(Effect.provide(BackgroundJob.layer)), }).pipe(Effect.provide(jobsLayer)),
) )
it.live("publishes jobs before starting immediately settling work", () => 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", () => 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", () => it.live("interrupts live work without promising settlement after the owning process-local scope closes", () =>

View file

@ -1,11 +1,12 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { CommandV2 } from "@opencode-ai/core/command" 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 { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
const it = testEffect(CommandV2.locationLayer) const it = testEffect(AppNodeBuilder.build(CommandV2.node))
describe("CommandV2", () => { describe("CommandV2", () => {
it.effect("applies command transforms and preserves later overrides", () => it.effect("applies command transforms and preserves later overrides", () =>

View file

@ -69,8 +69,8 @@ void invalidNodeReplacement
// @ts-expect-error Replacement cannot introduce a new error // @ts-expect-error Replacement cannot introduce a new error
LayerNode.compile(a, [[a, Layer.effect(A, Effect.fail(new OtherError()))]]) LayerNode.compile(a, [[a, Layer.effect(A, Effect.fail(new OtherError()))]])
// @ts-expect-error Node replacement cannot introduce a new error
const invalidNodeErrorReplacement = () => 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: [] })]]) LayerNode.compile(a, [[a, make({ service: A, layer: Layer.effect(A, Effect.fail(new OtherError())), deps: [] })]])
void invalidNodeErrorReplacement void invalidNodeErrorReplacement

View file

@ -1,5 +1,6 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project" import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
@ -21,7 +22,7 @@ const projectLayer = Layer.succeed(
commit: () => Effect.void, 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", () => { describe("Location", () => {
it.effect("resolves the current project and vcs information", () => it.effect("resolves the current project and vcs information", () =>

View file

@ -1,12 +1,10 @@
import fs from "fs/promises" import fs from "fs/promises"
import path from "path" import path from "path"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { NodeFileSystem } from "@effect/platform-node" import { Effect, Option } from "effect"
import { Effect, Layer, Option } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Npm } from "@opencode-ai/core/npm" import { Npm } from "@opencode-ai/core/npm"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { tmpdir } from "./fixture/tmpdir" import { tmpdir } from "./fixture/tmpdir"
const win = process.platform === "win32" const win = process.platform === "win32"
@ -21,12 +19,7 @@ const writePackage = (dir: string, pkg: Record<string, unknown>) =>
) )
const npmLayer = (cache: string) => const npmLayer = (cache: string) =>
Npm.layer.pipe( AppNodeBuilder.build(Npm.node, [[Global.node, Global.layerWith({ cache, state: path.join(cache, "state") })]])
Layer.provide(EffectFlock.layer),
Layer.provide(FSUtil.layer),
Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
Layer.provide(NodeFileSystem.layer),
)
describe("Npm.sanitize", () => { describe("Npm.sanitize", () => {
test("keeps normal scoped package specs unchanged", () => { test("keeps normal scoped package specs unchanged", () => {

View file

@ -1,6 +1,7 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { CommandV2 } from "@opencode-ai/core/command" 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 { Location } from "@opencode-ai/core/location"
import { CommandPlugin } from "@opencode-ai/core/plugin/command" import { CommandPlugin } from "@opencode-ai/core/plugin/command"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
@ -10,12 +11,9 @@ import { host } from "./host"
const directory = AbsolutePath.make("/repo/packages/app") const directory = AbsolutePath.make("/repo/packages/app")
const project = AbsolutePath.make("/repo") const project = AbsolutePath.make("/repo")
const locationLayer = Layer.succeed(Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project })))
const it = testEffect( const it = testEffect(
CommandV2.locationLayer.pipe( AppNodeBuilder.build(CommandV2.node, [[Location.node, locationLayer]]),
Layer.provide(
Layer.succeed(Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project }))),
),
),
) )
describe("CommandPlugin.Plugin", () => { describe("CommandPlugin.Plugin", () => {

View file

@ -1,5 +1,6 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { Policy } from "@opencode-ai/core/policy" import { Policy } from "@opencode-ai/core/policy"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
@ -7,11 +8,9 @@ import { location } from "./fixture/location"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
const it = testEffect( const it = testEffect(
Policy.locationLayer.pipe( AppNodeBuilder.build(Policy.node, [
Layer.provide( [Location.node, Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") })))],
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), ]),
),
),
) )
describe("Policy", () => { describe("Policy", () => {

View file

@ -1,6 +1,8 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Cause, Deferred, Effect, Exit, Layer, Queue } from "effect" import { Cause, Deferred, Effect, Exit, Layer, Queue } from "effect"
import { Config } from "@opencode-ai/core/config" 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 { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { Pty } from "@opencode-ai/core/pty" 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 configLayer = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
const it = testEffect( const it = testEffect(
Pty.layer.pipe( AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [
Layer.provide(configLayer), [Config.node, configLayer],
Layer.provideMerge(EventV2.defaultLayer), [Location.node, locationLayer],
Layer.provideMerge(locationLayer), ]),
),
) )
const ptyTest = process.platform === "win32" ? it.live.skip : it.live 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 configuredShell = process.platform === "win32" ? undefined : Bun.which("bash")
const configuredIt = testEffect( const configuredIt = testEffect(
Pty.layer.pipe( AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [
Layer.provide( [
Config.node,
Layer.mock(Config.Service)({ Layer.mock(Config.Service)({
entries: () => entries: () =>
Effect.succeed( Effect.succeed(
@ -215,10 +217,9 @@ const configuredIt = testEffect(
: [], : [],
), ),
}), }),
), ],
Layer.provideMerge(EventV2.defaultLayer), [Location.node, locationLayer],
Layer.provideMerge(locationLayer), ]),
),
) )
const configuredTest = process.platform === "win32" ? configuredIt.live.skip : configuredIt.live const configuredTest = process.platform === "win32" ? configuredIt.live.skip : configuredIt.live

View file

@ -1,12 +1,15 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PtyID } from "@opencode-ai/core/pty/schema" import { PtyID } from "@opencode-ai/core/pty/schema"
import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const it = testEffect(PtyTicket.layer) const it = testEffect(LayerNode.compile(PtyTicket.node))
const itExpiring = testEffect(Layer.effect(PtyTicket.Service, PtyTicket.make(5))) const itExpiring = testEffect(
LayerNode.compile(PtyTicket.node, [[PtyTicket.node, Layer.effect(PtyTicket.Service, PtyTicket.make(5))]]),
)
describe("PTY websocket tickets", () => { describe("PTY websocket tickets", () => {
it.live("consumes tickets once", () => it.live("consumes tickets once", () =>

View file

@ -1,11 +1,15 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { Reference } from "@opencode-ai/core/reference" import { Reference } from "@opencode-ai/core/reference"
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
import { SystemContext } from "@opencode-ai/core/system-context/index" import { SystemContext } from "@opencode-ai/core/system-context/index"
import { it } from "./lib/effect" import { it } from "./lib/effect"
const guidanceLayer = (referenceLayer: Layer.Layer<Reference.Service>) =>
AppNodeBuilder.build(ReferenceGuidance.node, [[Reference.node, referenceLayer]])
describe("ReferenceGuidance", () => { describe("ReferenceGuidance", () => {
it.effect("lists available references in the system context", () => it.effect("lists available references in the system context", () =>
Effect.gen(function* () { Effect.gen(function* () {
@ -17,23 +21,24 @@ describe("ReferenceGuidance", () => {
expect(generation.baseline).toContain("<path>/docs</path>") expect(generation.baseline).toContain("<path>/docs</path>")
expect(generation.baseline).toContain("<description>Use for product documentation</description>") expect(generation.baseline).toContain("<description>Use for product documentation</description>")
}).pipe( }).pipe(
Effect.provide(ReferenceGuidance.layer),
Effect.provide( Effect.provide(
Layer.mock(Reference.Service, { guidanceLayer(
list: () => Layer.mock(Reference.Service, {
Effect.succeed([ list: () =>
new Reference.Info({ Effect.succeed([
name: "docs", new Reference.Info({
path: AbsolutePath.make("/docs"), name: "docs",
description: "Use for product documentation",
source: Reference.LocalSource.make({
type: "local",
path: AbsolutePath.make("/docs"), path: AbsolutePath.make("/docs"),
description: "Use for product documentation", 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 guidance = yield* ReferenceGuidance.Service
const generation = yield* SystemContext.initialize(yield* guidance.load()) const generation = yield* SystemContext.initialize(yield* guidance.load())
expect(generation.baseline).toBe("") expect(generation.baseline).toBe("")
}).pipe( }).pipe(Effect.provide(guidanceLayer(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })))),
Effect.provide(ReferenceGuidance.layer),
Effect.provide(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })),
),
) )
it.effect("omits references without descriptions", () => it.effect("omits references without descriptions", () =>
@ -55,18 +57,19 @@ describe("ReferenceGuidance", () => {
const generation = yield* SystemContext.initialize(yield* guidance.load()) const generation = yield* SystemContext.initialize(yield* guidance.load())
expect(generation.baseline).toBe("") expect(generation.baseline).toBe("")
}).pipe( }).pipe(
Effect.provide(ReferenceGuidance.layer),
Effect.provide( Effect.provide(
Layer.mock(Reference.Service, { guidanceLayer(
list: () => Layer.mock(Reference.Service, {
Effect.succeed([ list: () =>
new Reference.Info({ Effect.succeed([
name: "docs", new Reference.Info({
path: AbsolutePath.make("/docs"), name: "docs",
source: Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") }), path: AbsolutePath.make("/docs"),
}), source: Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") }),
]), }),
}), ]),
}),
),
), ),
), ),
) )

View file

@ -4,6 +4,8 @@ import { Effect, Layer, Stream } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent" import { AgentV2 } from "@opencode-ai/core/agent"
import { asc, eq } from "drizzle-orm" import { asc, eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database" 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 { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql" import { EventTable } from "@opencode-ai/core/event/sql"
import { Location } from "@opencode-ai/core/location" 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 { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { SessionV1 } from "@opencode-ai/core/v1/session" import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Prompt } from "@opencode-ai/core/session/prompt" import { Prompt } from "@opencode-ai/core/session/prompt"
import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionProjector } from "@opencode-ai/core/session/projector"
@ -34,24 +35,11 @@ const projects = Layer.succeed(
commit: () => Effect.void, 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( const it = testEffect(
Layer.mergeAll( AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), [
Database.defaultLayer, [ProjectV2.node, projects],
EventV2.defaultLayer, [SessionExecution.node, SessionExecution.noopLayer],
projects, ]),
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
SessionExecution.noopLayer,
sessions,
),
) )
const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
const id = SessionV2.ID.create() const id = SessionV2.ID.create()
@ -248,9 +236,10 @@ describe("SessionV2.create", () => {
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
) )
const targetDatabase = Database.layerFromPath(path.join(tmp.path, "target.sqlite")) const targetDatabase = Database.layerFromPath(path.join(tmp.path, "target.sqlite"))
const targetEvents = EventV2.layer.pipe(Layer.provide(targetDatabase)) const targetLayer = AppNodeBuilder.build(
const targetProjector = SessionProjector.layer.pipe(Layer.provide(targetEvents), Layer.provide(targetDatabase)) LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node]),
const targetStore = SessionStore.layer.pipe(Layer.provide(targetDatabase)) [[Database.node, targetDatabase]],
)
yield* Effect.gen(function* () { yield* Effect.gen(function* () {
const db = (yield* Database.Service).db const db = (yield* Database.Service).db
@ -298,7 +287,7 @@ describe("SessionV2.create", () => {
[1, EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1)], [1, EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1)],
[2, EventV2.versionedType(SessionEvent.Prompted.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)))
}), }),
) )

View file

@ -1,9 +1,10 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer, Schema } from "effect" import { Effect, Layer, Schema } from "effect"
import { Database } from "@opencode-ai/core/database/database" 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 { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql" import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
@ -22,24 +23,11 @@ const projects = Layer.succeed(
commit: () => Effect.void, 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( const it = testEffect(
Layer.mergeAll( AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), [
Database.defaultLayer, [ProjectV2.node, projects],
EventV2.defaultLayer, [SessionExecution.node, SessionExecution.noopLayer],
projects, ]),
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
SessionExecution.noopLayer,
sessions,
),
) )
const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })

View file

@ -2,6 +2,8 @@ import { describe, expect } from "bun:test"
import { DateTime, Effect, Fiber, Layer, Stream } from "effect" import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database" 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 { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql" import { EventTable } from "@opencode-ai/core/event/sql"
import { SessionEvent } from "@opencode-ai/core/session/event" 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 { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { Prompt } from "@opencode-ai/core/session/prompt" import { Prompt } from "@opencode-ai/core/session/prompt"
import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionProjector } from "@opencode-ai/core/session/projector" 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( const it = testEffect(
Layer.mergeAll( AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), [
Database.defaultLayer, [SessionExecution.node, execution],
EventV2.defaultLayer, ]),
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
execution,
sessions,
),
) )
const sessionID = SessionV2.ID.make("ses_prompt_test") const sessionID = SessionV2.ID.make("ses_prompt_test")
const messageID = SessionMessage.ID.create() const messageID = SessionMessage.ID.create()

View file

@ -1,6 +1,8 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Tool } from "@opencode-ai/core/tool/tool" import { Tool } from "@opencode-ai/core/tool/tool"
import { AgentV2 } from "@opencode-ai/core/agent" 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 { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { SessionMessage } from "@opencode-ai/core/session/message" 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 registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
const it = testEffect(registry) const it = testEffect(registryLayer)
const integrated = testEffect(Layer.mergeAll(ApplicationTools.layer, registry)) const integrated = testEffect(
AppNodeBuilder.build(LayerNode.group([ApplicationTools.node, ToolRegistry.node]), [[ToolOutputStore.node, outputStore]]),
)
const identity = { const identity = {
agent: AgentV2.ID.make("build"), agent: AgentV2.ID.make("build"),
assistantMessageID: SessionMessage.ID.make("msg_registry"), assistantMessageID: SessionMessage.ID.make("msg_registry"),

View file

@ -2,6 +2,7 @@ import path from "path"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent" 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 { AbsolutePath } from "@opencode-ai/core/schema"
import { SkillV2 } from "@opencode-ai/core/skill" import { SkillV2 } from "@opencode-ai/core/skill"
import { SystemContext } from "@opencode-ai/core/system-context" import { SystemContext } from "@opencode-ai/core/system-context"
@ -28,7 +29,9 @@ const denied = SkillV2.Info.make({
}) })
const layer = (list: () => SkillV2.Info[]) => 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", () => { describe("SkillGuidance", () => {
it.effect("renders described agent skills and reconciles the complete available list", () => { it.effect("renders described agent skills and reconciles the complete available list", () => {

View file

@ -1,5 +1,6 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Cause, Effect, Exit, Schema, Scope } from "effect" 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 { SystemContext } from "@opencode-ai/core/system-context"
import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry"
import { testEffect } from "../lib/effect" 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", () => { describe("SystemContextRegistry", () => {
it.effect("loads empty system context when there are no entries", () => it.effect("loads empty system context when there are no entries", () =>

View file

@ -1,5 +1,6 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Exit, Fiber, Layer } from "effect" 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 { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionV2 } from "@opencode-ai/core/permission" import { PermissionV2 } from "@opencode-ai/core/permission"
@ -31,9 +32,6 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"), list: () => Effect.die("unused"),
}), }),
) )
const registry = AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode]), [
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
])
const question = Layer.succeed( const question = Layer.succeed(
QuestionV2.Service, QuestionV2.Service,
QuestionV2.Service.of({ QuestionV2.Service.of({
@ -46,8 +44,18 @@ const question = Layer.succeed(
list: () => Effect.die("unused"), list: () => Effect.die("unused"),
}), }),
) )
const tool = QuestionTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(question)) const toolNode = makeLocationNode({
const it = testEffect(Layer.mergeAll(permission, registry, question, tool)) 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", () => { describe("QuestionTool", () => {
it.effect("omits a denied built-in question and terminally settles a stale call", () => it.effect("omits a denied built-in question and terminally settles a stale call", () =>

View file

@ -1,6 +1,9 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { Database } from "@opencode-ai/core/database/database" 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 { EventV2 } from "@opencode-ai/core/event"
import { PermissionV2 } from "@opencode-ai/core/permission" import { PermissionV2 } from "@opencode-ai/core/permission"
import { Project } from "@opencode-ai/core/project" 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 { SessionTodo } from "@opencode-ai/core/session/todo"
import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite" import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite"
import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
@ -32,14 +36,19 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"), list: () => Effect.die("unused"),
}), }),
) )
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission)) const toolNode = makeLocationNode({
const tool = TodoWriteTool.layer.pipe( name: "test/todowrite-tool",
Layer.provide(registry), layer: TodoWriteTool.layer,
Layer.provide(permission), deps: [ToolRegistry.node, PermissionV2.node, SessionTodo.node],
Layer.provide(SessionTodo.defaultLayer), })
)
const it = testEffect( 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* () { const setup = Effect.gen(function* () {

View file

@ -1,11 +1,16 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Duration, Effect, Fiber, Layer, Schema } from "effect" import { Duration, Effect, Fiber, Layer, Schema } from "effect"
import * as TestClock from "effect/testing/TestClock" 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 { PermissionV2 } from "@opencode-ai/core/permission"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { WebFetchTool } from "@opencode-ai/core/tool/webfetch" import { WebFetchTool } from "@opencode-ai/core/tool/webfetch"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
@ -35,15 +40,19 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"), list: () => Effect.die("unused"),
}), }),
) )
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission)) const toolNode = makeLocationNode({
const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(http)) name: "test/webfetch-tool",
const it = testEffect(Layer.mergeAll(registry, permission, http, webfetch)) layer: WebFetchTool.layer,
const fetchWebfetch = WebFetchTool.layer.pipe( deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient],
Layer.provide(registry), })
Layer.provide(permission), const toolLayer = (replacements: LayerNode.Replacements = []) =>
Layer.provide(FetchHttpClient.layer), AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, toolNode]), [
) [PermissionV2.node, permission],
const live = testEffect(Layer.mergeAll(registry, permission, FetchHttpClient.layer, fetchWebfetch)) [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
...replacements,
])
const it = testEffect(toolLayer([[LayerNodePlatform.httpClient, http]]))
const live = testEffect(toolLayer())
const reset = () => { const reset = () => {
requests.length = 0 requests.length = 0

View file

@ -1,10 +1,15 @@
import { beforeEach, describe, expect, test } from "bun:test" import { beforeEach, describe, expect, test } from "bun:test"
import { Effect, Layer, Schema } from "effect" import { Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http" 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 { PermissionV2 } from "@opencode-ai/core/permission"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { WebSearchTool } from "@opencode-ai/core/tool/websearch" import { WebSearchTool } from "@opencode-ai/core/tool/websearch"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
@ -99,7 +104,6 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"), list: () => Effect.die("unused"),
}), }),
) )
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const websearchConfig = Layer.succeed( const websearchConfig = Layer.succeed(
WebSearchTool.ConfigService, WebSearchTool.ConfigService,
WebSearchTool.ConfigService.of({ WebSearchTool.ConfigService.of({
@ -120,13 +124,23 @@ const websearchConfig = Layer.succeed(
}, },
}), }),
) )
const websearch = WebSearchTool.layer.pipe( const websearchConfigNode = makeLocationNode({
Layer.provide(registry), service: WebSearchTool.ConfigService,
Layer.provide(permission), layer: websearchConfig,
Layer.provide(http), deps: [],
Layer.provide(websearchConfig), })
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", () => { describe("WebSearchTool registration", () => {
it.effect("registers websearch, asserts query permission, and calls Exa", () => it.effect("registers websearch, asserts query permission, and calls Exa", () =>