chore: merge dev into v2 (#34788)

Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com>
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Ben Guthrie <benjee.012@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
Co-authored-by: Max Anderson <max.a.anderson95@gmail.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
This commit is contained in:
James Long 2026-07-01 17:12:00 -04:00 committed by GitHub
commit 8c94e9005f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
590 changed files with 15772 additions and 5530 deletions

View file

@ -2,6 +2,8 @@ import { describe, expect } from "bun:test"
import { Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { EventV2 } from "@opencode-ai/core/event"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
@ -14,10 +16,9 @@ const testLocation = location({ directory: AbsolutePath.make("/project") })
const locationLayer = Layer.succeed(Location.Service, Location.Service.of(testLocation))
const it = testEffect(
AgentV2.locationLayer.pipe(
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
),
AppNodeBuilder.build(LayerNode.group([AgentV2.node, EventV2.node, Location.node]), [
[Location.node, locationLayer],
]) as unknown as Layer.Layer<unknown, never>,
)
describe("AgentV2", () => {

View file

@ -3,6 +3,8 @@ import { Effect, Fiber, Layer, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential"
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 { ModelV2 } from "@opencode-ai/core/model"
@ -21,13 +23,11 @@ const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
)
const it = testEffect(
Catalog.locationLayer.pipe(
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
Layer.provideMerge(Credential.defaultLayer),
),
const catalogLayer = AppNodeBuilder.build(
LayerNode.group([Catalog.node, EventV2.node, Credential.node, Integration.node, Policy.node]),
[[Location.node, locationLayer]],
)
const it = testEffect(catalogLayer)
describe("CatalogV2", () => {
it.effect("publishes an updated event after catalog changes", () =>
@ -47,11 +47,8 @@ describe("CatalogV2", () => {
it.effect("derives availability from active credentials without changing provider state", () => {
const integrationID = Integration.ID.make("test")
const layer = Catalog.locationLayer.pipe(
Layer.fresh,
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
Layer.provideMerge(Credential.defaultLayer.pipe(Layer.fresh)),
const localCatalogLayer = Layer.fresh(
AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node]), [[Location.node, locationLayer]]),
)
return Effect.gen(function* () {
@ -73,17 +70,16 @@ describe("CatalogV2", () => {
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
}).pipe(Effect.provide(layer))
}).pipe(Effect.provide(localCatalogLayer))
})
it.effect("derives availability from a provider's integration", () => {
const integrationID = Integration.ID.make("gateway")
const providerID = ProviderV2.ID.make("remote")
const layer = Catalog.locationLayer.pipe(
Layer.fresh,
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
Layer.provideMerge(Credential.defaultLayer.pipe(Layer.fresh)),
const localCatalogLayer = Layer.fresh(
AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [
[Location.node, locationLayer],
]),
)
return Effect.gen(function* () {
@ -102,7 +98,7 @@ describe("CatalogV2", () => {
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([providerID])
}).pipe(Effect.provide(layer))
}).pipe(Effect.provide(localCatalogLayer))
})
it.effect("projects environment connections without a catalog plugin", () =>

View file

@ -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", () =>

View file

@ -1,11 +1,12 @@
import { describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Effect, Layer, Schema } from "effect"
import { Effect, Schema } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Config } from "@opencode-ai/core/config"
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
import { EventV2 } from "@opencode-ai/core/event"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
@ -13,9 +14,7 @@ import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { agentHost, host } from "../plugin/host"
const it = testEffect(
Layer.mergeAll(AgentV2.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer)), FSUtil.defaultLayer),
)
const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node])))
const decode = Schema.decodeUnknownSync(Config.Info)
const defaultPermissions = [
{ action: "*", resource: "*", effect: "allow" },

View file

@ -1,10 +1,12 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer, Schema } from "effect"
import { Effect, Schema } from "effect"
import { CommandV2 } from "@opencode-ai/core/command"
import { Config } from "@opencode-ai/core/config"
import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
@ -13,7 +15,7 @@ import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { host } from "../plugin/host"
const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
const it = testEffect(AppNodeBuilder.build(LayerNode.group([CommandV2.node, FSUtil.node])))
const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigCommandPlugin.Plugin", () => {

View file

@ -5,6 +5,8 @@ import { Effect, Layer, Schema } from "effect"
import { FastCheck } from "effect/testing"
import { Config } from "@opencode-ai/core/config"
import { ConfigProvider } from "@opencode-ai/core/config/provider"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { FSUtil } from "@opencode-ai/core/fs-util"
@ -25,21 +27,19 @@ function testLayer(
projectDirectory = directory,
vcs?: Project.Vcs,
) {
return Config.locationLayer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Global.layerWith({ config: globalDirectory })),
Layer.provide(
Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{ projectDirectory: AbsolutePath.make(projectDirectory), vcs },
),
),
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{ projectDirectory: AbsolutePath.make(projectDirectory), vcs },
),
),
)
return AppNodeBuilder.build(LayerNode.group([Config.node, Policy.node]), [
[Location.node, locationLayer],
[Global.node, Global.layerWith({ config: globalDirectory })],
])
}
const provider = {

View file

@ -1,10 +1,11 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Credential } from "@opencode-ai/core/credential"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Integration } from "@opencode-ai/core/integration"
import { testEffect } from "./lib/effect"
const it = testEffect(Credential.defaultLayer)
const it = testEffect(LayerNode.compile(Credential.node))
describe("Credential", () => {
it.effect("stores, updates, lists, and removes credentials", () =>

View file

@ -15,6 +15,8 @@ import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migrat
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
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 { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
@ -268,7 +270,6 @@ describe("DatabaseMigration", () => {
yield* DatabaseMigration.applyOnly(db, [simplifySessionInputMigration])
const database = Layer.succeed(Database.Service, { db })
const events = EventV2.layer.pipe(Layer.provide(database))
yield* EventV2.Service.use((service) =>
service.publish(SessionV1.Event.Updated, {
sessionID: SessionSchema.ID.make("session"),
@ -284,7 +285,7 @@ describe("DatabaseMigration", () => {
}),
).pipe(
Effect.provide(
Layer.merge(events, SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))),
AppNodeBuilder.build(LayerNode.group([EventV2.node, SessionProjector.node]), [[Database.node, database]]),
),
)

View file

@ -15,7 +15,7 @@ class OtherError {
const tags = LayerNode.tags({ app: [] })
const make = tags.make("app")
const build = <A, E>(root: LayerNode.Node<A, E, any>) => LayerNode.compile(root)
const build = <A, E>(root: LayerNode.Node<A, E, any>) => LayerNode.compile(root) as Layer.Layer<A, E>
const aLayer = Layer.succeed(A, A.of({}))
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
const cLayer = Layer.effect(
@ -32,6 +32,8 @@ const b = make({ service: B, layer: bLayer, deps: [a] })
const c = make({ service: C, layer: cLayer, deps: [a, b] })
const failing = make({ service: A, layer: failingA, deps: [] })
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
const inputA = LayerNode.unbound(A, tags.values.app)
const inputDependent = make({ service: B, layer: bLayer, deps: [inputA] })
make({ name: "manual-a", layer: aLayer, deps: [] })
@ -49,20 +51,28 @@ make({ service: C, layer: cLayer, deps: [a] })
const closed = build(LayerNode.group([c]))
const closedWithError = build(LayerNode.group([dependent]))
const checkClosed: Layer.Layer<C> = closed
const checkError: Layer.Layer<B, LayerError> = closedWithError
const checkClosed: Layer.Layer<C, never, never> = closed
const checkError: Layer.Layer<B, LayerError, never> = closedWithError
void checkClosed
void checkError
LayerNode.replace(aLayer, Layer.succeed(A, A.of({})))
LayerNode.compile(a, [[a, Layer.succeed(A, A.of({}))]])
LayerNode.compile(a, [[a, make({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })]])
// @ts-expect-error Replacement must provide A
LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
LayerNode.compile(a, [[a, Layer.succeed(B, B.of({}))]])
// @ts-expect-error Node replacement must provide A
const invalidNodeReplacement = () => LayerNode.compile(a, [[a, b]])
void invalidNodeReplacement
// @ts-expect-error Replacement cannot introduce a new error
LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
LayerNode.compile(a, [[a, Layer.effect(A, Effect.fail(new OtherError()))]])
LayerNode.replace(bLayer, bLayer)
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
class TagA extends Context.Service<TagA, {}>()("test/TagA") {}
class TagB extends Context.Service<TagB, {}>()("test/TagB") {}

View file

@ -13,7 +13,7 @@ class App extends Context.Service<App, { readonly run: Effect.Effect<string[]> }
const tags = LayerNode.tags({ app: [] })
const make = tags.make("app")
const build = <A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) =>
LayerNode.compile(root, new Map(replacements?.map((item) => [item.source, item.replacement]))) as Layer.Layer<A, E>
LayerNode.compile(root, replacements) as Layer.Layer<A, E>
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
const greetingLayer = Layer.effect(
Greeting,
@ -63,21 +63,20 @@ describe("layer node", () => {
expect(await Effect.runPromise(program)).toEqual(["first", "second"])
})
test("requires unbound nodes to be bound before compilation", async () => {
test("requires unbound nodes to be replaced before compilation", async () => {
const unbound = LayerNode.unbound(Value, tags.values.app)
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [unbound] })
const tree = LayerNode.group([greeting])
expect(() => LayerNode.compile(tree)).toThrow("Unbound layer node: test/LayerNodeValue")
const bound = LayerNode.bind(tree, unbound, value)
const layer = LayerNode.compile(bound) as Layer.Layer<Greeting>
const layer = LayerNode.compile(tree, [[unbound, value]]) as Layer.Layer<Greeting>
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(layer))
expect(await Effect.runPromise(program)).toBe("hello production")
})
test("replaces a layer by identity", async () => {
test("replaces a node with a closed layer", async () => {
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
const program = Effect.map(Greeting, (item) => item.value).pipe(
Effect.provide(build(LayerNode.group([greeting]), [LayerNode.replace(valueLayer, replacement)])),
Effect.provide(build(LayerNode.group([greeting]), [[value, replacement]])),
)
expect(await Effect.runPromise(program)).toBe("hello simulation")
})
@ -94,7 +93,7 @@ describe("layer node", () => {
const left = make({ service: Left, layer: leftLayer, deps: [value] })
const right = make({ service: Right, layer: rightLayer, deps: [value] })
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
const layer = build(LayerNode.group([left, right]), [LayerNode.replace(valueLayer, replacement)])
const layer = build(LayerNode.group([left, right]), [[value, replacement]])
const program = Effect.gen(function* () {
return [(yield* Left).value, (yield* Right).value]
}).pipe(Effect.provide(layer))
@ -103,22 +102,62 @@ describe("layer node", () => {
test("does not acquire an unused replacement", async () => {
let acquisitions = 0
const other = Layer.succeed(Value, Value.of({ value: "other" }))
const other = make({ service: Left, layer: Layer.succeed(Left, Left.of({ value: "other" })), deps: [] })
const replacement = Layer.effect(
Value,
Left,
Effect.sync(() => {
acquisitions++
return Value.of({ value: "replacement" })
return Left.of({ value: "replacement" })
}),
)
await Effect.runPromise(
Effect.map(Greeting, (item) => item.value).pipe(
Effect.provide(build(LayerNode.group([greeting]), [LayerNode.replace(other, replacement)])),
Effect.provide(build(LayerNode.group([greeting]), [[other, replacement]])),
),
)
expect(acquisitions).toBe(0)
})
test("replaces a node without acquiring its dependencies", async () => {
let acquisitions = 0
const dependencyLayer = Layer.effect(
Value,
Effect.sync(() => {
acquisitions++
return Value.of({ value: "dependency" })
}),
)
const dependency = make({ service: Value, layer: dependencyLayer, deps: [] })
const original = make({ service: Greeting, layer: greetingLayer, deps: [dependency] })
const replacement = make({
service: Greeting,
layer: Layer.succeed(Greeting, Greeting.of({ value: "replacement" })),
deps: [],
})
const program = Effect.map(Greeting, (item) => item.value).pipe(
Effect.provide(build(LayerNode.group([original]), [[original, replacement]])),
)
expect(await Effect.runPromise(program)).toBe("replacement")
expect(acquisitions).toBe(0)
})
test("applies later replacements inside earlier replacement nodes", async () => {
const original = make({ service: Greeting, layer: greetingLayer, deps: [value] })
const replacement = make({ service: Greeting, layer: greetingLayer, deps: [value] })
const program = Effect.map(Greeting, (item) => item.value).pipe(
Effect.provide(
build(LayerNode.group([original]), [
[original, replacement],
[value, Layer.succeed(Value, Value.of({ value: "replacement dependency" }))],
]),
),
)
expect(await Effect.runPromise(program)).toBe("hello replacement dependency")
})
test("hoists and compiles tagged graphs", async () => {
const tags = LayerNode.tags({ location: ["global"], global: [] })
const global = tags.make("global")

View file

@ -1,8 +1,8 @@
import { describe, expect, test } from "bun:test"
import { Context, Effect, Layer, LayerMap, Option } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Node } 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 { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import type { LocationError, LocationServices } from "@opencode-ai/core/location-services"
@ -31,7 +31,7 @@ describe("node build", () => {
expect(await Effect.runPromise(program)).toBe("plain")
})
test("detects cycles through a bound location service map", () => {
test("detects cycles through a replaced location service map", async () => {
const a = Node.makeGlobalNode({
service: CycleA,
layer: Layer.effect(CycleA, Effect.as(LocationServiceMap.Service, CycleA.of({}))),
@ -45,26 +45,28 @@ describe("node build", () => {
),
deps: [a],
})
const mapEffect = Effect.gen(function* () {
const service = yield* CycleB
return yield* LayerMap.make(
(ref: Location.Ref) =>
Layer.succeed(
Location.Service,
Location.Service.of({
directory: ref.directory,
workspaceID: ref.workspaceID,
project: { id: Project.ID.global, directory: service.directory },
}),
),
{ idleTimeToLive: "1 minute" },
)
}) as unknown as Effect.Effect<LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>, never, CycleB>
const mapLayer = Layer.effect(LocationServiceMap.Service, mapEffect)
const mapLayer = Layer.effect(
LocationServiceMap.Service,
Effect.gen(function* () {
const service = yield* CycleB
return yield* LayerMap.make(
(ref: Location.Ref) =>
Layer.succeed(
Location.Service,
Location.Service.of({
directory: ref.directory,
workspaceID: ref.workspaceID,
project: { id: Project.ID.global, directory: service.directory },
}),
),
{ idleTimeToLive: "1 minute" },
)
}) as unknown as Effect.Effect<LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>, never, CycleB>,
)
const map = Node.makeGlobalNode({ service: LocationServiceMap.Service, layer: mapLayer, deps: [b] })
const graph = LayerNode.bind(LayerNode.group([a]), LocationServiceMap.node, map)
expect(() => AppNodeBuilder.build(graph)).toThrow("Cycle detected in layer tree")
expect(() => AppNodeBuilder.build(LayerNode.group([a]), [[LocationServiceMap.node, map]])).toThrow(
"Cycle detected in layer tree",
)
})
test("shares top-level project with location services", async () => {
@ -81,10 +83,10 @@ describe("node build", () => {
})
}),
)
const layer = AppNodeBuilder.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
LayerNode.replace(Project.layer, projectLayer),
])
const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
const layer = AppNodeBuilder.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
[Project.node, projectLayer],
])
const program = Effect.gen(function* () {
yield* Project.Service
const locations = yield* LocationServiceMap.Service

View file

@ -6,6 +6,8 @@ import { Session } from "@opencode-ai/schema/session"
import { SessionEvent } from "@opencode-ai/schema/session-event"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
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 { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
@ -76,9 +78,10 @@ const durableData = (sessionID: Session.ID, text: string) => ({
messageID: SessionV1.MessageID.ascending(`msg_${text}`),
})
const eventLayer = Layer.mergeAll(EventV2.layerWith().pipe(Layer.provide(Database.defaultLayer)), Database.defaultLayer)
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
const itWithoutLocation = testEffect(eventLayer)
const it = testEffect(
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, Location.node]), [[Location.node, locationLayer]]),
)
const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node])))
describe("EventV2", () => {
it.effect("publishes events with the current location", () =>
@ -462,7 +465,7 @@ describe("EventV2", () => {
pause
? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead)))
: Effect.void,
}).pipe(Layer.provide(Database.defaultLayer))
}).pipe(Layer.provide(LayerNode.compile(Database.node)))
yield* Effect.gen(function* () {
const events = yield* EventV2.Service
@ -477,7 +480,7 @@ describe("EventV2", () => {
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[0, durableData(aggregateID, "during handoff")],
])
}).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer)))
}).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer)))
}),
)

View file

@ -2,6 +2,8 @@ import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Deferred, Effect, Fiber, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
@ -11,14 +13,17 @@ import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { it } from "./lib/effect"
function provide(directory: string, filesystem = FSUtil.defaultLayer) {
function provide(directory: string, filesystemLayer = LayerNode.compile(FSUtil.node)) {
const activeLocation = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
)
const resolution = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
const mutation = FileMutation.layer.pipe(Layer.provide(filesystem))
return Effect.provide(Layer.mergeAll(resolution, mutation))
return Effect.provide(
AppNodeBuilder.build(LayerNode.group([LocationMutation.node, FileMutation.node]), [
[Location.node, activeLocation],
[FSUtil.node, filesystemLayer],
]),
)
}
function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) {
@ -359,5 +364,5 @@ function instrumentWrites(run: <E>(write: Effect.Effect<void, E>, target: string
run(filesystem.writeFileString(target, content, options), target),
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
}

View file

@ -1,11 +1,12 @@
import { describe, test, expect } from "bun:test"
import { Effect, Layer, FileSystem } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { Effect, FileSystem } from "effect"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { testEffect } from "../lib/effect"
import path from "path"
const live = Layer.merge(FSUtil.defaultLayer, NodeFileSystem.layer)
const live = LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem]))
const { effect: it } = testEffect(live)
describe("FSUtil", () => {

View file

@ -4,10 +4,11 @@ import fs from "fs/promises"
import path from "path"
import { Deferred, Duration, Effect, Fiber, Layer, Option, Stream } 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 { FSUtil } from "@opencode-ai/core/fs-util"
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
import { Git } from "@opencode-ai/core/git"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
@ -18,7 +19,7 @@ const describeWatcher = Watcher.hasNativeBinding() && !process.env.CI ? describe
type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
const it = testEffect(Layer.mergeAll(FSUtil.defaultLayer, EventV2.defaultLayer))
const it = testEffect(AppNodeBuilder.build(LayerNode.group([FSUtil.node, EventV2.node])))
const configLayer = Layer.succeed(
Config.Service,
@ -33,11 +34,10 @@ function provide(directory: string, vcs?: Location.Interface["vcs"]) {
Location.Service.of(location({ directory: AbsolutePath.make(directory) }, { vcs })),
)
return Effect.provide(
Watcher.layer.pipe(
Layer.provide(configLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(locationLayer),
),
AppNodeBuilder.build(Watcher.node, [
[Config.node, configLayer],
[Location.node, locationLayer],
]),
)
}
@ -188,7 +188,7 @@ describeWatcher("Watcher", () => {
yield* noUpdate((event) => event.file === file, fs.writeFileString(file, "gone")).pipe(
Effect.provideService(EventV2.Service, events),
)
}).pipe(Effect.provide(Layer.mergeAll(FSUtil.defaultLayer, EventV2.defaultLayer))),
}).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([FSUtil.node, EventV2.node])))),
)
it.live("ignores .git/index changes", () =>
@ -220,10 +220,7 @@ describeWatcher("Watcher", () => {
yield* Effect.promise(() => $`git branch ${branch}`.cwd(directory).quiet())
expect(
yield* nextUpdate((event) => event.file === head, fs.writeFileString(head, `ref: refs/heads/${branch}\n`)),
).toEqual({
file: head,
event: "change",
})
).toMatchObject({ file: head })
}),
{ git: true },
),

View file

@ -1,7 +1,7 @@
import fs from "fs/promises"
import os from "os"
import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Effect } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Global } from "@opencode-ai/core/global"
@ -30,7 +30,7 @@ const testGlobal = Global.layerWith({
log: os.tmpdir(),
})
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(FSUtil.defaultLayer))
const testLayer = AppNodeBuilder.build(EffectFlock.node, [[Global.node, testGlobal]])
async function job() {
if (msg.ready) await fs.writeFile(msg.ready, String(process.pid))

View file

@ -0,0 +1,206 @@
import { OpenAIResponsesLanguageModel } from "@opencode-ai/core/github-copilot/responses/openai-responses-language-model"
import { convertToOpenAIResponsesInput } from "@opencode-ai/core/github-copilot/responses/convert-to-openai-responses-input"
import { describe, test, expect, mock } from "bun:test"
import type { LanguageModelV3Prompt } from "@ai-sdk/provider"
const TEST_PROMPT: LanguageModelV3Prompt = [{ role: "user", content: [{ type: "text", text: "Hello" }] }]
function createMockFetch(body: unknown) {
return mock(
async () => new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } }),
)
}
function createModel(fetchFn: ReturnType<typeof mock>) {
return new OpenAIResponsesLanguageModel("test-model", {
provider: "copilot",
url: () => "https://api.test.com/responses",
headers: () => ({ Authorization: "Bearer test-token" }),
fetch: fetchFn as any,
})
}
// GitHub Copilot's Responses model echoes item metadata (itemId, reasoningEncryptedContent,
// responseId, ...) under the "copilot" providerOptions/providerMetadata namespace, matching the
// namespace request options already use. It used to echo this metadata under "openai" (a leftover
// from forking the OpenAI Responses model), which left it unreachable by anything reading the
// "copilot" namespace and let stale itemIds slip past stripping meant for that namespace.
describe("doGenerate", () => {
test("attaches item metadata under the copilot namespace, not openai", async () => {
const mockFetch = createMockFetch({
id: "resp_1",
created_at: 0,
model: "gpt-5.5",
output: [
{
type: "reasoning",
id: "rs_1",
encrypted_content: "enc_1",
summary: [{ type: "summary_text", text: "thinking..." }],
},
{
type: "message",
role: "assistant",
id: "msg_1",
content: [{ type: "output_text", text: "Hello there", annotations: [] }],
},
{
type: "function_call",
call_id: "call_1",
name: "bash",
arguments: "{}",
id: "fc_1",
},
],
usage: { input_tokens: 10, output_tokens: 5 },
})
const model = createModel(mockFetch)
const { content, providerMetadata } = await model.doGenerate({
prompt: TEST_PROMPT,
includeRawChunks: false,
} as any)
const reasoning = content.find((part: any) => part.type === "reasoning") as any
expect(reasoning.providerMetadata?.copilot?.itemId).toBe("rs_1")
expect(reasoning.providerMetadata?.copilot?.reasoningEncryptedContent).toBe("enc_1")
expect(reasoning.providerMetadata?.openai).toBeUndefined()
const text = content.find((part: any) => part.type === "text") as any
expect(text.providerMetadata?.copilot?.itemId).toBe("msg_1")
expect(text.providerMetadata?.openai).toBeUndefined()
const toolCall = content.find((part: any) => part.type === "tool-call") as any
expect(toolCall.providerMetadata?.copilot?.itemId).toBe("fc_1")
expect(toolCall.providerMetadata?.openai).toBeUndefined()
expect(providerMetadata?.copilot?.responseId).toBe("resp_1")
expect(providerMetadata?.openai).toBeUndefined()
})
})
describe("convertToOpenAIResponsesInput", () => {
test("echoes a stale tool-call itemId from the copilot namespace as the function_call id", async () => {
const { input } = await convertToOpenAIResponsesInput({
prompt: [
{
role: "assistant",
content: [
{
type: "tool-call",
toolCallId: "call_1",
toolName: "bash",
input: { command: "ls" },
providerOptions: { copilot: { itemId: "fc_999" } },
},
],
},
],
systemMessageMode: "system",
store: false,
})
expect(input).toEqual([
{
type: "function_call",
call_id: "call_1",
name: "bash",
arguments: JSON.stringify({ command: "ls" }),
id: "fc_999",
},
])
})
test("omits the function_call id once the stale copilot itemId has been stripped", async () => {
const { input } = await convertToOpenAIResponsesInput({
prompt: [
{
role: "assistant",
content: [
{
type: "tool-call",
toolCallId: "call_1",
toolName: "bash",
input: { command: "ls" },
providerOptions: {},
},
],
},
],
systemMessageMode: "system",
store: false,
})
expect((input[0] as any).id).toBeUndefined()
})
test("preserves reasoning items keyed by the copilot namespace instead of dropping them", async () => {
const { input, warnings } = await convertToOpenAIResponsesInput({
prompt: [
{
role: "assistant",
content: [
{
type: "reasoning",
text: "thinking...",
providerOptions: { copilot: { itemId: "rs_1", reasoningEncryptedContent: "enc_1" } },
},
],
},
],
systemMessageMode: "system",
store: false,
})
expect(warnings).toEqual([])
expect(input).toEqual([
{
type: "reasoning",
id: "rs_1",
encrypted_content: "enc_1",
summary: [{ type: "summary_text", text: "thinking..." }],
},
])
})
test("drops reasoning items with no copilot itemId and warns, as before", async () => {
const { input, warnings } = await convertToOpenAIResponsesInput({
prompt: [
{
role: "assistant",
content: [{ type: "reasoning", text: "thinking...", providerOptions: {} }],
},
],
systemMessageMode: "system",
store: false,
})
expect(input).toEqual([])
expect(warnings).toHaveLength(1)
expect(warnings[0]).toMatchObject({
message: expect.stringContaining("Non-OpenAI reasoning parts are not supported"),
})
})
test("reads imageDetail from the copilot namespace on user file parts", async () => {
const { input } = await convertToOpenAIResponsesInput({
prompt: [
{
role: "user",
content: [
{
type: "file",
mediaType: "image/png",
data: "aGVsbG8=",
providerOptions: { copilot: { imageDetail: "high" } },
},
],
},
],
systemMessageMode: "system",
store: false,
})
expect((input[0] as any).content[0].detail).toBe("high")
})
})

View file

@ -2,6 +2,8 @@ import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import fs from "fs/promises"
import path from "path"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { InstructionContext } from "@opencode-ai/core/instruction-context"
@ -15,6 +17,17 @@ import { testEffect } from "./lib/effect"
const it = testEffect(Layer.empty)
const instructionLayer = (input: {
config: string
locationServiceLayer: Layer.Layer<Location.Service>
filesystemLayer?: Layer.Layer<FSUtil.Service>
}) =>
AppNodeBuilder.build(LayerNode.group([SystemContextRegistry.node, InstructionContext.node]), [
[Global.node, Global.layerWith({ config: input.config })],
[Location.node, input.locationServiceLayer],
...(input.filesystemLayer ? [[FSUtil.node, input.filesystemLayer] as const] : []),
])
describe("InstructionContext", () => {
it.live("loads global and upward project AGENTS.md files as one aggregate context", () =>
Effect.acquireRelease(
@ -41,19 +54,19 @@ describe("InstructionContext", () => {
const load = SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(FSUtil.defaultLayer),
Effect.provide(Global.layerWith({ config: global })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{ projectDirectory: AbsolutePath.make(project) },
instructionLayer({
config: global,
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{ projectDirectory: AbsolutePath.make(project) },
),
),
),
),
}),
),
)
@ -107,14 +120,14 @@ describe("InstructionContext", () => {
yield* Effect.promise(() => fs.writeFile(file, ""))
const context = yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(FSUtil.defaultLayer),
Effect.provide(Global.layerWith({ config: path.join(tmp.path, "global") })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(tmp.path) })),
),
instructionLayer({
config: path.join(tmp.path, "global"),
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(tmp.path) })),
),
}),
),
)
@ -133,14 +146,18 @@ describe("InstructionContext", () => {
FSUtil.Service.of({ ...fs, up: () => Effect.fail(new FSUtil.FileSystemError({ method: "up" })) }),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const context = yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(failingFS),
Effect.provide(Global.layerWith({ config: "/global" })),
Effect.provide(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") }))),
instructionLayer({
config: "/global",
filesystemLayer: failingFS,
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
),
}),
),
)
@ -169,14 +186,18 @@ describe("InstructionContext", () => {
}),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const context = yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(racingFS),
Effect.provide(Global.layerWith({ config: "/global" })),
Effect.provide(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") }))),
instructionLayer({
config: "/global",
filesystemLayer: racingFS,
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
),
}),
),
)
@ -208,20 +229,21 @@ describe("InstructionContext", () => {
}),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(observingFS),
Effect.provide(Global.layerWith({ config: "/global" })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(
location({ directory: AbsolutePath.make("/repo/") }, { projectDirectory: AbsolutePath.make("/repo") }),
instructionLayer({
config: "/global",
filesystemLayer: observingFS,
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(
location({ directory: AbsolutePath.make("/repo/") }, { projectDirectory: AbsolutePath.make("/repo") }),
),
),
),
}),
),
)
@ -241,18 +263,20 @@ describe("InstructionContext", () => {
yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(
Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.sync(() => ((scanned = true), [])) })),
instructionLayer({
config: "/global",
filesystemLayer: Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.sync(() => ((scanned = true), [])) })),
),
).pipe(Layer.provide(LayerNode.compile(FSUtil.node))),
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
),
).pipe(Layer.provide(FSUtil.defaultLayer)),
),
Effect.provide(Global.layerWith({ config: "/global" })),
Effect.provide(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") }))),
}),
),
Effect.ensuring(
Effect.sync(() => {
@ -271,23 +295,25 @@ describe("InstructionContext", () => {
let scanned = false
yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(
Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.sync(() => ((scanned = true), [])) })),
instructionLayer({
config: "/global",
filesystemLayer: Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.sync(() => ((scanned = true), [])) })),
),
).pipe(Layer.provide(LayerNode.compile(FSUtil.node))),
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make("/outside") },
{ projectDirectory: AbsolutePath.make("/repo") },
),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer)),
),
Effect.provide(Global.layerWith({ config: "/global" })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(
location({ directory: AbsolutePath.make("/outside") }, { projectDirectory: AbsolutePath.make("/repo") }),
),
),
}),
),
)

View file

@ -1,10 +1,11 @@
import { describe, expect } from "bun:test"
import { Job } from "@opencode-ai/core/job"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Deferred, Effect, Exit, Fiber, Scope } from "effect"
import { SessionSchema } from "@opencode-ai/core/session/schema"
import { testEffect } from "./lib/effect"
const it = testEffect(Job.layer)
const it = testEffect(AppNodeBuilder.build(Job.node))
describe("Job", () => {
it.live("tracks process-local work through explicit observation", () =>

View file

@ -2,7 +2,6 @@ import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Exit, Layer } from "effect"
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Location } from "@opencode-ai/core/location"
@ -13,20 +12,12 @@ import { it } from "./lib/effect"
const provide = (directory: string) =>
Effect.provide(
LayerNode.compile(
LayerNode.bind(
FileSystem.node,
LayerNode.compile(FileSystem.node, [
[
Location.node,
makeLocationNode({
service: Location.Service,
layer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
),
deps: [],
}),
),
),
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
],
]),
)
const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>

View file

@ -1,11 +1,13 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { DateTime, Effect, Equal, Hash, Layer, Schema } from "effect"
import { DateTime, Effect, Equal, Hash, Schema } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ModelV2 } from "@opencode-ai/core/model"
@ -17,34 +19,13 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
import { toolDefinitions, waitForTool } from "./lib/tool"
import { FSUtil } from "../src/fs-util"
import { Credential } from "../src/credential"
import { Database } from "../src/database/database"
import { EventV2 } from "../src/event"
import { Global } from "../src/global"
import { ModelsDev } from "../src/models-dev"
import { Npm } from "../src/npm"
import { Project } from "../src/project"
import { Reference } from "../src/reference"
import { ToolRegistry } from "../src/tool/registry"
const it = testEffect(
Layer.merge(
Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer),
locationServiceMapLayer.pipe(
Layer.provide(
Layer.mergeAll(
Project.defaultLayer,
EventV2.defaultLayer,
Credential.defaultLayer.pipe(Layer.fresh),
Npm.defaultLayer,
ModelsDev.defaultLayer,
FSUtil.defaultLayer,
Global.defaultLayer,
),
),
),
),
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])),
)
describe("LocationServiceMap", () => {

View file

@ -2,7 +2,6 @@ import fs from "fs/promises"
import path from "path"
import { describe, expect, test } from "bun:test"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
@ -13,20 +12,12 @@ import { it } from "./lib/effect"
function provide(directory: string) {
return Effect.provide(
LayerNode.compile(
LayerNode.bind(
LocationMutation.node,
LayerNode.compile(LocationMutation.node, [
[
Location.node,
makeLocationNode({
service: Location.Service,
layer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
),
deps: [],
}),
),
),
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
],
]),
)
}

View file

@ -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", () =>

View file

@ -1,11 +1,12 @@
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
import { Effect, Layer, Ref, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { EventV2 } from "@opencode-ai/core/event"
import { it } from "./lib/effect"
import { readFile, rm, writeFile, utimes, mkdir } from "fs/promises"
import path from "path"
@ -87,13 +88,13 @@ const makeMockClient = (state: Ref.Ref<MockState>) =>
)
const buildLayer = (state: Ref.Ref<MockState>) =>
// Layer.fresh is required: ModelsDev.layer is a module-level Layer constant,
// Layer.fresh is required because the ModelsDev implementation is a module-level Layer constant,
// and Effect.provide uses a process-global MemoMap by default — without fresh,
// every test would reuse the cachedInvalidateWithTTL state from the first run.
Layer.fresh(ModelsDev.layer).pipe(
Layer.provide(Layer.succeed(HttpClient.HttpClient, makeMockClient(state))),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.fresh(
AppNodeBuilder.build(ModelsDev.node, [
[LayerNodePlatform.httpClient, Layer.succeed(HttpClient.HttpClient, makeMockClient(state))],
]),
)
const writeCacheText = (text: string, mtimeMs?: number) =>

View file

@ -3,11 +3,11 @@ import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import { eq } from "drizzle-orm"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
import { Database } from "@opencode-ai/core/database/database"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Git } from "@opencode-ai/core/git"
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 { Job } from "@opencode-ai/core/job"
import { Project } from "@opencode-ai/core/project"
@ -15,47 +15,23 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
import { ProjectDirectories } from "@opencode-ai/core/project/directories"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionStore } from "@opencode-ai/core/session/store"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const project = Project.layer.pipe(
Layer.provide(Database.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(ProjectDirectories.defaultLayer),
)
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
Layer.provide(locationServiceMapLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(project),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(SessionExecution.noopLayer),
)
const layer = MoveSession.layer.pipe(
Layer.provide(Database.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(project),
Layer.provide(SessionStore.defaultLayer),
)
const it = testEffect(
Layer.mergeAll(
layer,
Database.defaultLayer,
EventV2.defaultLayer,
ProjectDirectories.defaultLayer,
project,
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
sessions,
AppNodeBuilder.build(
LayerNode.group([
MoveSession.node,
Database.node,
EventV2.node,
ProjectDirectories.node,
Project.node,
SessionProjector.node,
SessionStore.node,
]),
),
)

View file

@ -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<string, unknown>) =>
)
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", () => {

View file

@ -2,6 +2,8 @@ import { describe, expect } from "bun:test"
import { Deferred, Effect, Fiber, Layer } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
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 { Job } from "@opencode-ai/core/job"
import { Location } from "@opencode-ai/core/location"
@ -12,9 +14,7 @@ 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 { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionStore } from "@opencode-ai/core/session/store"
import { eq } from "drizzle-orm"
import { location } from "./fixture/location"
@ -24,25 +24,19 @@ const current = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
)
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
Layer.provide(locationServiceMapLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(SessionExecution.noopLayer),
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([
Database.node,
EventV2.node,
SessionStore.node,
PermissionSaved.node,
AgentV2.node,
PermissionV2.node,
]),
[[Location.node, current]],
),
)
const layer = PermissionV2.locationLayer.pipe(
Layer.provideMerge(Database.defaultLayer),
Layer.provideMerge(SessionStore.defaultLayer),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(current),
Layer.provideMerge(sessions),
Layer.provideMerge(SessionExecution.noopLayer),
Layer.provideMerge(PermissionSaved.defaultLayer),
)
const it = testEffect(layer)
function setup(rules: PermissionV2.Ruleset = []) {
return Effect.gen(function* () {

View file

@ -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,13 +11,11 @@ import { host } from "./host"
const directory = AbsolutePath.make("/repo/packages/app")
const project = AbsolutePath.make("/repo")
const it = testEffect(
CommandV2.locationLayer.pipe(
Layer.provide(
Layer.succeed(Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project }))),
),
),
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory }, { projectDirectory: project })),
)
const it = testEffect(AppNodeBuilder.build(CommandV2.node, [[Location.node, locationLayer]]))
describe("CommandPlugin.Plugin", () => {
it.effect("registers built-in init and review commands", () =>

View file

@ -1,37 +1,56 @@
import { AgentV2 } from "@opencode-ai/core/agent"
import { AISDK } from "@opencode-ai/core/aisdk"
import { Catalog } from "@opencode-ai/core/catalog"
import { CommandV2 } from "@opencode-ai/core/command"
import { Credential } from "@opencode-ai/core/credential"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Integration } from "@opencode-ai/core/integration"
import { Location } from "@opencode-ai/core/location"
import { Npm } from "@opencode-ai/core/npm"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
import { Reference } from "@opencode-ai/core/reference"
import { SkillV2 } from "@opencode-ai/core/skill"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { tempLocationLayer } from "../fixture/location"
export const PluginTestLayer = Layer.mergeAll(FileSystem.locationLayer, PluginV2.locationLayer).pipe(
Layer.provideMerge(
Layer.mergeAll(
Credential.defaultLayer,
EventV2.defaultLayer,
FetchHttpClient.layer,
FSUtil.defaultLayer,
Global.defaultLayer,
Layer.succeed(
Npm.Service,
Npm.Service.of({
add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
install: () => Effect.void,
which: () => Effect.succeed(undefined),
}),
),
RepositoryCache.defaultLayer,
SkillDiscovery.defaultLayer,
Ripgrep.defaultLayer,
tempLocationLayer,
),
),
const npmLayer = Layer.succeed(
Npm.Service,
Npm.Service.of({
add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
install: () => Effect.void,
which: () => Effect.succeed(undefined),
}),
)
export const PluginTestLayer = AppNodeBuilder.build(
LayerNode.group([
FileSystem.node,
FSUtil.node,
Location.node,
Npm.node,
Credential.node,
EventV2.node,
LayerNodePlatform.httpClient,
PluginV2.node,
AgentV2.node,
AISDK.node,
Catalog.node,
CommandV2.node,
Integration.node,
PluginRuntime.node,
Reference.node,
SkillV2.node,
ToolRegistry.toolsNode,
]),
[
[Location.node, tempLocationLayer],
[Npm.node, npmLayer],
],
) as unknown as Layer.Layer<unknown, never>

View file

@ -3,32 +3,27 @@ import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential"
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 { Flag } from "@opencode-ai/core/flag/flag"
import { Location } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
import { Policy } from "@opencode-ai/core/policy"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { catalogHost, host, integrationHost } from "./host"
const events = EventV2.defaultLayer
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
)
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
const connections = Credential.defaultLayer.pipe(Layer.fresh)
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
const catalog = Catalog.layer.pipe(
Layer.provide(Layer.mergeAll(events, locationLayer, policy, connections, integrations)),
)
const layer = Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer)
const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, EventV2.node]), [
[Location.node, locationLayer],
])
const it = testEffect(layer)
describe("ModelsDevPlugin", () => {
@ -164,7 +159,7 @@ describe("ModelsDevPlugin", () => {
connections: [],
}),
])
}).pipe(Effect.provide(ModelsDev.defaultLayer)),
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
(previous) =>
Effect.sync(() => {
Flag.OPENCODE_MODELS_PATH = previous.path
@ -242,7 +237,7 @@ describe("ModelsDevPlugin", () => {
},
}),
])
}).pipe(Effect.provide(ModelsDev.defaultLayer)),
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
(previous) =>
Effect.sync(() => {
Flag.OPENCODE_MODELS_PATH = previous.path

View file

@ -6,6 +6,7 @@ import os from "os"
import path from "path"
import { fileURLToPath } from "url"
import { AISDK } from "@opencode-ai/core/aisdk"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
@ -17,7 +18,7 @@ import { PluginTestLayer } from "./fixture"
const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
const fixtureProviderPath = fileURLToPath(fixtureProvider)
const it = testEffect(PluginTestLayer)
const itWithAISDK = testEffect(AISDK.locationLayer.pipe(Layer.provideMerge(PluginTestLayer)))
const itWithAISDK = testEffect(Layer.mergeAll(PluginTestLayer, AppNodeBuilder.build(AISDK.node)))
function npmEntrypoint(entrypoint?: string) {
return Npm.Service.of({

View file

@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { EventV2 } from "@opencode-ai/core/event"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
@ -16,8 +17,12 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const events = yield* EventV2.Service
const integration = yield* Integration.Service
yield* OpencodePlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration))
yield* OpencodePlugin.effect(host).pipe(
Effect.provideService(EventV2.Service, events),
Effect.provideService(Integration.Service, integration),
)
})
function required<T>(value: T | undefined): T {

View file

@ -5,10 +5,10 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Location } from "@opencode-ai/core/location"
import { Effect } from "effect"
import { SkillPlugin } from "@opencode-ai/core/plugin/skill"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SkillV2 } from "@opencode-ai/core/skill"
import { Effect } from "effect"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { host } from "./host"
@ -25,7 +25,7 @@ describe("SkillPlugin.Plugin", () => {
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
),
Effect.provide(FSUtil.defaultLayer),
Effect.provide(AppNodeBuilder.build(FSUtil.node)),
Effect.provide(NodeFileSystem.layer),
)
const skills = yield* skill.list()

View file

@ -1,11 +1,9 @@
import { describe, expect } from "bun:test"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { EventV2 } from "@opencode-ai/core/event"
import { Integration } from "@opencode-ai/core/integration"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model"
import { Policy } from "@opencode-ai/core/policy"
import { VariantPlugin } from "@opencode-ai/core/plugin/variant"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
@ -14,21 +12,11 @@ import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { catalogHost, host } from "./host"
const events = EventV2.defaultLayer
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
)
const connections = Credential.defaultLayer.pipe(Layer.fresh)
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
const catalog = Catalog.layer.pipe(
Layer.provide(
Layer.mergeAll(events, locationLayer, Policy.layer.pipe(Layer.provide(locationLayer)), connections, integrations),
),
)
const it = testEffect(
Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer),
)
const it = testEffect(AppNodeBuilder.build(Catalog.node, [[Location.node, locationLayer]]))
describe("VariantPlugin", () => {
it.effect("adds GLM 5.2 variants after catalog sources", () =>

View file

@ -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,12 @@ import { location } from "./fixture/location"
import { testEffect } from "./lib/effect"
const it = testEffect(
Policy.locationLayer.pipe(
Layer.provide(
AppNodeBuilder.build(Policy.node, [
[
Location.node,
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
),
),
],
]),
)
describe("Policy", () => {

View file

@ -3,9 +3,10 @@ import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import { eq } from "drizzle-orm"
import { Effect, Fiber, Layer, Stream } from "effect"
import { Effect, Fiber, Stream } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Git } from "@opencode-ai/core/git"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
@ -16,15 +17,8 @@ import { ProjectDirectories } from "@opencode-ai/core/project/directories"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const copyLayer = ProjectCopy.layer.pipe(
Layer.provide(Database.defaultLayer),
Layer.provide(ProjectDirectories.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Git.defaultLayer),
)
const it = testEffect(
Layer.mergeAll(copyLayer, Database.defaultLayer, EventV2.defaultLayer, ProjectDirectories.defaultLayer),
AppNodeBuilder.build(LayerNode.group([ProjectCopy.node, Database.node, EventV2.node, ProjectDirectories.node])),
)
function abs(input: string) {

View file

@ -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

View file

@ -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", () =>

View file

@ -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<Reference.Service>) =>
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("<path>/docs</path>")
expect(generation.baseline).toContain("<description>Use for product documentation</description>")
}).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") }),
}),
]),
}),
),
),
),
)

View file

@ -1,16 +1,18 @@
import { describe, expect } from "bun:test"
import { Effect, Exit, Layer, Scope } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/core/global"
import { Reference } from "@opencode-ai/core/reference"
import { Repository } from "@opencode-ai/core/repository"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { EventV2 } from "@opencode-ai/core/event"
import { it } from "./lib/effect"
const cache = Layer.mock(RepositoryCache.Service, {
ensure: () => Effect.die("unexpected Git materialization"),
})
const referenceLayer = AppNodeBuilder.build(Reference.node, [[RepositoryCache.node, cache]])
describe("Reference", () => {
it.effect("registers normalized sources for the owning scope", () =>
@ -32,12 +34,7 @@ describe("Reference", () => {
yield* Scope.close(scope, Exit.void)
expect(yield* references.list()).toEqual([])
}).pipe(
Effect.provide(Reference.layer),
Effect.provide(cache),
Effect.provide(EventV2.defaultLayer),
Effect.provide(Global.defaultLayer),
),
}).pipe(Effect.provide(referenceLayer)),
)
it.effect("derives Git paths without exposing cache operations", () =>
@ -54,13 +51,7 @@ describe("Reference", () => {
source,
}),
])
}).pipe(
Effect.scoped,
Effect.provide(Reference.layer),
Effect.provide(cache),
Effect.provide(EventV2.defaultLayer),
Effect.provide(Global.defaultLayer),
),
}).pipe(Effect.scoped, Effect.provide(referenceLayer)),
)
it.effect("preserves configured Git descriptions", () =>
@ -82,12 +73,6 @@ describe("Reference", () => {
source,
}),
])
}).pipe(
Effect.scoped,
Effect.provide(Reference.layer),
Effect.provide(cache),
Effect.provide(EventV2.defaultLayer),
Effect.provide(Global.defaultLayer),
),
}).pipe(Effect.scoped, Effect.provide(referenceLayer)),
)
})

View file

@ -3,12 +3,11 @@ import fs from "fs/promises"
import path from "path"
import { pathToFileURL } from "url"
import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Git } from "@opencode-ai/core/git"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Global } from "@opencode-ai/core/global"
import { Repository } from "@opencode-ai/core/repository"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { git, gitRemote } from "./fixture/git"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
@ -89,15 +88,9 @@ describe("RepositoryCache", () => {
})
function cacheLayer(root: string) {
const dependencies = Layer.mergeAll(
Global.layerWith({ state: path.join(root, "state"), repos: path.join(root, "repos") }),
FSUtil.defaultLayer,
)
return RepositoryCache.layer.pipe(
Layer.provide(EffectFlock.layer.pipe(Layer.provide(dependencies))),
Layer.provide(Git.defaultLayer),
Layer.provide(dependencies),
)
return AppNodeBuilder.build(RepositoryCache.node, [
[Global.node, Global.layerWith({ state: path.join(root, "state"), repos: path.join(root, "repos") })],
])
}
function withRemote<A, E, R>(body: (fixture: Awaited<ReturnType<typeof gitRemote>>) => Effect.Effect<A, E, R>) {

View file

@ -3,6 +3,8 @@ import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/llm"
import { OpenAIChat } from "@opencode-ai/llm/protocols"
import { Config } from "@opencode-ai/core/config"
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 { Job } from "@opencode-ai/core/job"
import { Location } from "@opencode-ai/core/location"
@ -60,24 +62,14 @@ const locations = Layer.effect(
) as unknown as Layer.Layer<LocationServices>,
),
)
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
Layer.provide(locations),
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]),
[
[LocationServiceMap.node, locations],
[ProjectV2.node, projects],
[SessionExecution.node, SessionExecution.noopLayer],
],
),
)

View file

@ -3,6 +3,9 @@ import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/llm"
import { OpenAIChat } from "@opencode-ai/llm/protocols"
import { Config } from "@opencode-ai/core/config"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
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 { SessionCompaction } from "@opencode-ai/core/session/compaction"
@ -37,17 +40,13 @@ const client = Layer.mock(LLMClient.Service)({
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
const models = Layer.mock(SessionRunnerModel.Service)({ resolve: () => Effect.succeed(model) })
const it = testEffect(
Layer.mergeAll(
Database.defaultLayer,
EventV2.defaultLayer,
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
SessionCompaction.layer.pipe(
Layer.provide(client),
Layer.provide(config),
Layer.provide(models),
Layer.provide(EventV2.defaultLayer),
),
AppNodeBuilder.build(
LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionCompaction.node]),
[
[llmClient, client],
[Config.node, config],
[SessionRunnerModel.node, models],
],
),
)

View file

@ -4,6 +4,8 @@ import { DateTime, 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 { Job } from "@opencode-ai/core/job"
@ -14,7 +16,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"
@ -36,24 +37,13 @@ const projects = Layer.succeed(
commit: () => Effect.void,
}),
)
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
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") })
@ -366,9 +356,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
@ -416,7 +407,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)))
}),
)

View file

@ -1,10 +1,11 @@
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 { Job } from "@opencode-ai/core/job"
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"
@ -23,24 +24,13 @@ const projects = Layer.succeed(
commit: () => Effect.void,
}),
)
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
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") })

View file

@ -4,7 +4,6 @@ import { asc, eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
import { ModelV2 } from "@opencode-ai/core/model"
@ -25,13 +24,7 @@ import { testEffect } from "./lib/effect"
import { Snapshot } from "@opencode-ai/core/snapshot"
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node])))
const sessionsLayer = AppNodeBuilder.build(
LayerNode.bind(
SessionV2.node,
SessionExecution.node,
makeGlobalNode({ service: SessionExecution.Service, layer: SessionExecution.noopLayer, deps: [] }),
),
)
const sessionsLayer = AppNodeBuilder.build(SessionV2.node, [[SessionExecution.node, SessionExecution.noopLayer]])
const sessionID = SessionV2.ID.make("ses_projector_test")
const created = DateTime.makeUnsafe(0)
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }

View file

@ -2,6 +2,8 @@ import { describe, expect } from "bun:test"
import { DateTime, Effect, Fiber, Layer, Schema, 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 { Job } from "@opencode-ai/core/job"
@ -12,7 +14,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"
@ -45,23 +46,10 @@ const execution = Layer.succeed(
awaitIdle: () => Effect.void,
}),
)
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
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")

View file

@ -3,6 +3,9 @@ import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal"
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
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 { Job } from "@opencode-ai/core/job"
@ -13,7 +16,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 { Snapshot } from "@opencode-ai/core/snapshot"
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
import { SessionTitle } from "@opencode-ai/core/session/title"
@ -25,6 +27,7 @@ import { SessionRunner } from "@opencode-ai/core/session/runner"
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionStore } from "@opencode-ai/core/session/store"
import { Location } from "@opencode-ai/core/location"
@ -61,8 +64,6 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"),
}),
)
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const agents = AgentV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
const model = OpenAIChat.route
.with({
endpoint: { baseURL: "https://api.openai.com/v1" },
@ -71,30 +72,24 @@ const model = OpenAIChat.route
})
.model({ id: "gpt-4o-mini" })
const models = SessionRunnerModel.layerWith(() => Effect.succeed(model))
const systemContext = SystemContextRegistry.layer
const location = Location.layer({ directory: AbsolutePath.make("/project") }).pipe(Layer.provide(Project.defaultLayer))
const systemContext = AppNodeBuilder.build(SystemContextRegistry.node)
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
const runner = SessionRunnerLLM.defaultLayer.pipe(
Layer.provide(SessionCompaction.layer),
Layer.provide(SessionTitle.layer),
Layer.provide(Snapshot.noopLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(client),
Layer.provide(registry),
Layer.provide(models),
Layer.provide(systemContext),
Layer.provide(location),
Layer.provide(agents),
Layer.provide(skillGuidance),
Layer.provide(referenceGuidance),
Layer.provide(mcpGuidance),
Layer.provide(config),
)
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
[Snapshot.node, Snapshot.noopLayer],
[LayerNodePlatform.llmClient, client],
[SessionRunnerModel.node, models],
[SystemContextRegistry.node, systemContext],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],
[McpGuidance.node, mcpGuidance],
[Config.node, config],
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
])
const execution = Layer.effect(
SessionExecution.Service,
Effect.gen(function* () {
@ -110,35 +105,38 @@ const execution = Layer.effect(
awaitIdle: coordinator.awaitIdle,
})
}),
).pipe(Layer.provide(runner))
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
Layer.provide(locationServiceMapLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(execution),
)
).pipe(Layer.provide(runnerLayer))
const it = testEffect(
Layer.mergeAll(
Database.defaultLayer,
EventV2.defaultLayer,
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
executor,
client,
permission,
agents,
registry,
models,
systemContext,
location,
skillGuidance,
config,
runner,
execution,
sessions,
AppNodeBuilder.build(
LayerNode.group([
Database.node,
EventV2.node,
SessionProjector.node,
SessionStore.node,
AgentV2.node,
ToolRegistry.node,
SessionRunnerModel.node,
SystemContextRegistry.node,
SkillGuidance.node,
ReferenceGuidance.node,
Config.node,
Snapshot.node,
SessionRunnerLLM.node,
SessionV2.node,
]),
[
[LayerNodePlatform.llmClient, client],
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
[SessionRunnerModel.node, models],
[SystemContextRegistry.node, systemContext],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],
[Config.node, config],
[Snapshot.node, Snapshot.noopLayer],
[SessionExecution.node, execution],
],
),
)
const sessionID = SessionV2.ID.make("ses_runner_recorded")

View file

@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
import { Tool } from "@opencode-ai/core/tool/tool"
import { AgentV2 } from "@opencode-ai/core/agent"
import type { PermissionV2 } from "@opencode-ai/core/permission"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
@ -27,8 +28,8 @@ const outputStore = Layer.mock(ToolOutputStore.Service, {
)
},
})
const registry = ToolRegistry.layer.pipe(Layer.provide(outputStore))
const it = testEffect(registry)
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
const it = testEffect(registryLayer)
const identity = {
agent: AgentV2.ID.make("build"),
assistantMessageID: SessionMessage.ID.make("msg_registry"),

View file

@ -11,6 +11,10 @@ import {
} from "@opencode-ai/llm"
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
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 { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Job } from "@opencode-ai/core/job"
import { PermissionV2 } from "@opencode-ai/core/permission"
@ -20,7 +24,6 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
import { QuestionV2 } from "@opencode-ai/core/question"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { Snapshot } from "@opencode-ai/core/snapshot"
import { ContextSnapshotDecodeError } from "@opencode-ai/core/session/error"
import { SessionEvent } from "@opencode-ai/core/session/event"
@ -61,7 +64,6 @@ import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream }
import { asc, eq } from "drizzle-orm"
import { testEffect } from "./lib/effect"
const questions = QuestionV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
const requests: LLMRequest[] = []
let response: LLMEvent[] = []
let responses: LLMEvent[][] | undefined
@ -125,8 +127,6 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"),
}),
)
const registry = ToolRegistry.layer.pipe(Layer.provide(permission), Layer.provide(ToolOutputStore.defaultLayer))
const agents = AgentV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
const echo = Layer.effectDiscard(
ToolRegistry.Service.use((registry) =>
registry.register({
@ -156,7 +156,8 @@ const echo = Layer.effectDiscard(
}),
}),
),
).pipe(Layer.provide(registry))
)
const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] })
let modelResolveHook = Effect.void
let currentModel = model
const models = SessionRunnerModel.layerWith((session) =>
@ -196,8 +197,7 @@ const systemContext = Layer.effectDiscard(
}),
),
),
).pipe(Layer.provideMerge(SystemContextRegistry.layer))
const location = Location.layer({ directory: AbsolutePath.make("/project") }).pipe(Layer.provide(Project.defaultLayer))
).pipe(Layer.provideMerge(AppNodeBuilder.build(SystemContextRegistry.node)))
const skillGuidance = Layer.mock(SkillGuidance.Service, {
load: (agent) =>
Effect.succeed(
@ -232,24 +232,19 @@ const config = Layer.succeed(
]),
}),
)
const runner = SessionRunnerLLM.layer.pipe(
Layer.provide(SessionCompaction.layer),
Layer.provide(SessionTitle.layer),
Layer.provide(Snapshot.noopLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(client),
Layer.provide(registry),
Layer.provide(models),
Layer.provide(systemContext),
Layer.provide(location),
Layer.provide(agents),
Layer.provide(skillGuidance),
Layer.provide(referenceGuidance),
Layer.provide(mcpGuidance),
Layer.provide(config),
)
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
[Snapshot.node, Snapshot.noopLayer],
[LayerNodePlatform.llmClient, client],
[SessionRunnerModel.node, models],
[SystemContextRegistry.node, systemContext],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],
[PermissionV2.node, permission],
[Config.node, config],
[McpGuidance.node, mcpGuidance],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
])
const execution = Layer.effect(
SessionExecution.Service,
Effect.gen(function* () {
@ -265,36 +260,42 @@ const execution = Layer.effect(
awaitIdle: coordinator.awaitIdle,
})
}),
).pipe(Layer.provide(runner))
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
Layer.provide(locationServiceMapLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(execution),
)
).pipe(Layer.provide(runnerLayer))
const it = testEffect(
Layer.mergeAll(
Database.defaultLayer,
EventV2.defaultLayer,
questions,
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
client,
permission,
agents,
registry,
echo,
models,
systemContext,
location,
skillGuidance,
config,
runner,
execution,
sessions,
AppNodeBuilder.build(
LayerNode.group([
Database.node,
EventV2.node,
QuestionV2.node,
SessionProjector.node,
SessionStore.node,
AgentV2.node,
ToolRegistry.node,
ToolRegistry.toolsNode,
echoNode,
SessionRunnerModel.node,
SystemContextRegistry.node,
SkillGuidance.node,
ReferenceGuidance.node,
Config.node,
Snapshot.node,
SessionRunnerLLM.node,
SessionExecution.node,
SessionV2.node,
]),
[
[LayerNodePlatform.llmClient, client],
[PermissionV2.node, permission],
[SessionRunnerModel.node, models],
[SystemContextRegistry.node, systemContext],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],
[Snapshot.node, Snapshot.noopLayer],
[SessionExecution.node, execution],
[Config.node, config],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
],
),
)
const sessionID = SessionV2.ID.make("ses_runner_test")

View file

@ -3,6 +3,9 @@ import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/llm"
import { OpenAIChat } from "@opencode-ai/llm/protocols"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
@ -34,21 +37,13 @@ const client = Layer.mock(LLMClient.Service)({
generate: () => Effect.die("unused"),
})
const models = Layer.mock(SessionRunnerModel.Service)({ resolve: () => Effect.succeed(model) })
const agents = AgentV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
const it = testEffect(
Layer.mergeAll(
Database.defaultLayer,
EventV2.defaultLayer,
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
agents,
SessionTitle.layer.pipe(
Layer.provide(client),
Layer.provide(models),
Layer.provide(agents),
Layer.provide(EventV2.defaultLayer),
Layer.provide(Database.defaultLayer),
),
AppNodeBuilder.build(
LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, AgentV2.node, SessionTitle.node]),
[
[llmClient, client],
[SessionRunnerModel.node, models],
],
),
)

View file

@ -1,7 +1,8 @@
import { describe, expect } from "bun:test"
import { asc, eq } from "drizzle-orm"
import { DateTime, Effect, Layer, Schema } from "effect"
import { DateTime, Effect, Schema } from "effect"
import { Database } from "@opencode-ai/core/database/database"
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 { ModelV2 } from "@opencode-ai/core/model"
@ -16,7 +17,7 @@ import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionTable, SessionMessageTable } from "@opencode-ai/core/session/sql"
import { testEffect } from "./lib/effect"
const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionProjector.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Database.node, EventV2.node, SessionProjector.node])))
const timestamp = DateTime.makeUnsafe(1)
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }

View file

@ -1,6 +1,8 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } 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 { Job } from "@opencode-ai/core/job"
import { Location } from "@opencode-ai/core/location"
@ -10,7 +12,6 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionStore } from "@opencode-ai/core/session/store"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { testEffect } from "./lib/effect"
const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
@ -21,23 +22,13 @@ const projects = Layer.mock(ProjectV2.Service, {
const execution = Layer.mock(SessionExecution.Service, {
awaitIdle: (sessionID) => Effect.sync(() => awaited.push(sessionID)),
})
const sessions = SessionV2.layer.pipe(
Layer.provide(Job.layer),
Layer.provide(locationServiceMapLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(projects),
Layer.provide(execution),
)
const it = testEffect(
Layer.mergeAll(
Database.defaultLayer,
EventV2.defaultLayer,
projects,
SessionProjector.defaultLayer,
SessionStore.defaultLayer,
sessions,
AppNodeBuilder.build(
LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]),
[
[ProjectV2.node, projects],
[SessionExecution.node, execution],
],
),
)

View file

@ -3,7 +3,9 @@ import path from "path"
import { describe, expect, test } from "bun:test"
import { Effect, Layer } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Global } from "@opencode-ai/core/global"
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
import { tmpdir } from "./fixture/tmpdir"
@ -27,15 +29,14 @@ async function pull(skills: unknown[], files: Record<string, string> = {}, cache
),
),
)
const layer = SkillDiscovery.layer.pipe(
Layer.provide(http),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Global.layerWith({ cache: tmp.path })),
)
const skillDiscoveryLayer = AppNodeBuilder.build(SkillDiscovery.node, [
[LayerNodePlatform.httpClient, http],
[Global.node, Global.layerWith({ cache: tmp.path })],
])
const directories = await Effect.runPromise(
Effect.gen(function* () {
return yield* (yield* SkillDiscovery.Service).pull(base)
}).pipe(Effect.provide(layer)),
}).pipe(Effect.provide(skillDiscoveryLayer)),
)
return { tmp, requests, directories }
}

View file

@ -27,7 +27,7 @@ const discovery = Layer.succeed(
)
const it = testEffect(
AppNodeBuilder.build(LayerNode.group([SkillV2.node, AgentV2.node, EventV2.node]), [
LayerNode.replace(SkillDiscovery.layer, discovery),
[SkillDiscovery.node, discovery],
]),
)

View file

@ -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"
@ -35,7 +36,9 @@ const manual = 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", () => {

View file

@ -3,12 +3,9 @@ import { describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
import { Config } from "@opencode-ai/core/config"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Git } from "@opencode-ai/core/git"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Global } from "@opencode-ai/core/global"
import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Snapshot } from "@opencode-ai/core/snapshot"
import { Hash } from "@opencode-ai/core/util/hash"
@ -118,9 +115,7 @@ describe("Snapshot", () => {
return (yield* Location.Service).project.id
}).pipe(
Effect.provide(
Location.layer(Location.Ref.make({ directory: AbsolutePath.make(project) })).pipe(
Layer.provide(Project.defaultLayer),
),
AppNodeBuilder.build(Location.boundNode(Location.Ref.make({ directory: AbsolutePath.make(project) }))),
),
)
expect(
@ -172,16 +167,10 @@ describe("Snapshot", () => {
})
function snapshotLayer(data: string, directory: string) {
const location = Location.layer(Location.Ref.make({ directory: AbsolutePath.make(directory) })).pipe(
Layer.provide(Project.defaultLayer),
)
return Snapshot.layer.pipe(
Layer.provide(location),
Layer.provide(Config.locationLayer.pipe(Layer.provide(location))),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Global.layerWith({ data, config: path.join(data, "config") })),
)
return AppNodeBuilder.build(Snapshot.node, [
[Location.node, Location.boundNode(Location.Ref.make({ directory: AbsolutePath.make(directory) }))],
[Global.node, Global.layerWith({ data, config: path.join(data, "config") })],
])
}
function read(file: string) {

View file

@ -1,7 +1,6 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import * as TestClock from "effect/testing/TestClock"
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 { Location } from "@opencode-ai/core/location"
@ -28,14 +27,12 @@ const locationLayer = Layer.succeed(
),
),
)
const locationNode = makeLocationNode({ service: Location.Service, layer: locationLayer, deps: [] })
const builtInsNode = LayerNode.bind(
LayerNode.group([SystemContextBuiltIns.node, SystemContextRegistry.node]),
Location.node,
locationNode,
)
const builtInsNode = LayerNode.group([SystemContextBuiltIns.node, SystemContextRegistry.node])
const it = testEffect(
AppNodeBuilder.build(builtInsNode, [LayerNode.replace(Global.layer, Global.layerWith({ config: "/global" }))]),
AppNodeBuilder.build(builtInsNode, [
[Location.node, locationLayer],
[Global.node, Global.layerWith({ config: "/global" })],
]),
)
const instructionFS = Layer.effect(
FSUtil.Service,
@ -51,8 +48,9 @@ const instructionFS = Layer.effect(
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const itWithInstructions = testEffect(
AppNodeBuilder.build(builtInsNode, [
LayerNode.replace(FSUtil.layer, instructionFS),
LayerNode.replace(Global.layer, Global.layerWith({ config: "/global" })),
[Location.node, locationLayer],
[FSUtil.node, instructionFS],
[Global.node, Global.layerWith({ config: "/global" })],
]),
)

View file

@ -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", () =>

View file

@ -2,6 +2,8 @@ import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Deferred, Effect, Exit, Fiber, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
@ -10,6 +12,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { ApplyPatchTool } from "@opencode-ai/core/tool/apply-patch"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
@ -81,26 +84,34 @@ const filesystem = Layer.effect(
},
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) => {
const activeLocation = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
)
const resolution = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
const mutation = FileMutation.layer.pipe(Layer.provide(filesystem))
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const patch = ApplyPatchTool.layer.pipe(
Layer.provide(registry),
Layer.provide(permission),
Layer.provide(resolution),
Layer.provide(mutation),
Layer.provide(filesystem),
)
return Effect.gen(function* () {
return yield* body(yield* ToolRegistry.Service)
}).pipe(Effect.provide(Layer.mergeAll(registry, resolution, mutation, patch)))
}).pipe(
Effect.provide(
AppNodeBuilder.build(
LayerNode.group([
ToolRegistry.node,
ToolRegistry.toolsNode,
LocationMutation.node,
FileMutation.node,
ApplyPatchTool.node,
]),
[
[FSUtil.node, filesystem],
[Location.node, activeLocation],
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
],
),
),
)
}
const call = (patchText: string, id = "call-apply-patch") => ({

View file

@ -3,6 +3,8 @@ import path from "path"
import { fileURLToPath } from "url"
import { describe, expect, test } from "bun:test"
import { Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
@ -11,6 +13,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { EditTool } from "@opencode-ai/core/tool/edit"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
@ -71,26 +74,34 @@ const filesystem = Layer.effect(
Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeFileString(target, content, options))),
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) => {
const activeLocation = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
)
const resolution = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
const mutation = FileMutation.layer.pipe(Layer.provide(filesystem))
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const edit = EditTool.layer.pipe(
Layer.provide(registry),
Layer.provide(permission),
Layer.provide(resolution),
Layer.provide(mutation),
Layer.provide(filesystem),
)
return Effect.gen(function* () {
return yield* body(yield* ToolRegistry.Service)
}).pipe(Effect.provide(Layer.mergeAll(registry, resolution, mutation, edit)))
}).pipe(
Effect.provide(
AppNodeBuilder.build(
LayerNode.group([
ToolRegistry.node,
ToolRegistry.toolsNode,
LocationMutation.node,
FileMutation.node,
EditTool.node,
]),
[
[FSUtil.node, filesystem],
[Location.node, activeLocation],
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
],
),
),
)
}
const call = (input: typeof EditTool.Input.Type, id = "call-edit") => ({

View file

@ -1,6 +1,8 @@
import { describe, expect } from "bun:test"
import path from "path"
import { Cause, Effect, Exit, Fiber, Layer, Option } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Config } from "@opencode-ai/core/config"
@ -28,14 +30,14 @@ const withStore = <A, E, R>(
}),
)
: Layer.empty
const store = ToolOutputStore.layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(global),
Layer.provide(configured),
)
const store = AppNodeBuilder.build(LayerNode.group([ToolOutputStore.node, FSUtil.node]), [
[Global.node, global],
[Config.node, configured],
])
return Effect.gen(function* () {
return yield* body({ root: tmp.path, store: yield* ToolOutputStore.Service, fs: yield* FSUtil.Service })
}).pipe(Effect.provide(Layer.mergeAll(store, FSUtil.defaultLayer)))
}).pipe(Effect.provide(store))
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
@ -185,11 +187,11 @@ describe("ToolOutputStore", () => {
writeFileString: () => Effect.never,
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
const store = ToolOutputStore.layer.pipe(
Layer.provide(blockedFilesystem),
Layer.provide(Global.layerWith({ data: root.path })),
)
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const store = AppNodeBuilder.build(ToolOutputStore.nodeWithoutConfig, [
[Global.node, Global.layerWith({ data: root.path })],
[FSUtil.node, blockedFilesystem],
])
const exit = yield* Effect.gen(function* () {
const service = yield* ToolOutputStore.Service
const fiber = yield* service

View file

@ -1,10 +1,13 @@
import { describe, expect } from "bun:test"
import { Effect, Exit, Fiber, Layer } from "effect"
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"
import { QuestionV2 } from "@opencode-ai/core/question"
import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { QuestionTool } from "@opencode-ai/core/tool/question"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { testEffect } from "./lib/effect"
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
@ -28,7 +31,6 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"),
}),
)
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const question = Layer.succeed(
QuestionV2.Service,
QuestionV2.Service.of({
@ -41,8 +43,13 @@ 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 it = testEffect(
AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, QuestionTool.node]), [
[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", () =>

View file

@ -1,12 +1,13 @@
import { describe, expect } from "bun:test"
import { NodeFileSystem } from "@effect/platform-node"
import path from "path"
import { Effect, FileSystem, Layer } from "effect"
import { Effect, FileSystem } from "effect"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
import { testEffect } from "./lib/effect"
const it = testEffect(Layer.merge(FSUtil.defaultLayer, NodeFileSystem.layer))
const it = testEffect(LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem])))
const fixture = Effect.gen(function* () {
const fs = yield* FSUtil.Service
const files = yield* FileSystem.FileSystem

View file

@ -3,6 +3,8 @@ import path from "path"
import { Effect, Exit, Layer, PlatformError } from "effect"
import { Config } from "@opencode-ai/core/config"
import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
@ -14,6 +16,7 @@ import { Global } from "@opencode-ai/core/global"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { location } from "./fixture/location"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { ReadTool } from "@opencode-ai/core/tool/read"
import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
import { testEffect } from "./lib/effect"
@ -69,9 +72,8 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"),
}),
)
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed(configEntries) }))
const image = Image.layer.pipe(Layer.provide(config))
const imageLayer = AppNodeBuilder.build(Image.node, [[Config.node, config]])
const testFileSystem = Layer.effect(
FSUtil.Service,
FSUtil.Service.use((fs) =>
@ -92,11 +94,10 @@ const testFileSystem = Layer.effect(
}),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
const infrastructure = Layer.mergeAll(
testFileSystem,
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(process.cwd()) }))),
Global.layerWith({ data: Global.Path.data }),
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(process.cwd()) })),
)
const mutation = Layer.succeed(
LocationMutation.Service,
@ -128,28 +129,20 @@ const unavailableImage = Layer.succeed(
Image.Service,
Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }),
)
const read = ReadTool.layer.pipe(
Layer.provide(registry),
Layer.provide(reader),
Layer.provide(permission),
Layer.provide(config),
Layer.provide(image),
Layer.provide(mutation),
Layer.provide(infrastructure),
)
const it = testEffect(Layer.mergeAll(registry, reader, permission, config, image, mutation, infrastructure, read))
const unavailableRead = ReadTool.layer.pipe(
Layer.provide(registry),
Layer.provide(reader),
Layer.provide(permission),
Layer.provide(config),
Layer.provide(unavailableImage),
Layer.provide(mutation),
Layer.provide(infrastructure),
)
const itWithoutResizer = testEffect(
Layer.mergeAll(registry, reader, permission, config, unavailableImage, mutation, infrastructure, unavailableRead),
)
const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, ReadTool.node]), [
[ReadToolFileSystem.node, reader],
[PermissionV2.node, permission],
[Config.node, config],
[Image.node, imageLayer],
[LocationMutation.node, mutation],
[FSUtil.node, testFileSystem],
[Location.node, locationLayer],
[Global.node, Global.layerWith({ data: Global.Path.data })],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
])
const it = testEffect(readLayer(imageLayer))
const itWithoutResizer = testEffect(readLayer(unavailableImage))
const sessionID = SessionV2.ID.make("ses_read_tool_test")
describe("ReadTool", () => {

View file

@ -118,23 +118,23 @@ const executionNode = makeGlobalNode({
})
const layer = AppNodeBuilder.build(
LayerNode.bind(
LayerNode.group([
Database.node,
EventV2.node,
Job.node,
ToolOutputStore.cleanupNode,
SessionV2.node,
PluginRuntime.providerNode,
LocationServiceMap.node,
filesystem,
FSUtil.node,
Global.node,
]),
LayerNode.group([
Database.node,
EventV2.node,
Job.node,
ToolOutputStore.cleanupNode,
SessionV2.node,
SessionExecution.node,
executionNode,
),
[LayerNode.replace(PermissionV2.layer, permission)],
PluginRuntime.providerNode,
LocationServiceMap.node,
filesystem,
FSUtil.node,
Global.node,
]),
[
[SessionExecution.node, executionNode],
[PermissionV2.node, permission],
],
)
const it = testEffect(layer)

View file

@ -2,13 +2,15 @@ import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
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"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { SkillV2 } from "@opencode-ai/core/skill"
import { SkillTool } from "@opencode-ai/core/tool/skill"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { tmpdir } from "./fixture/tmpdir"
import { it } from "./lib/effect"
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
@ -63,14 +65,14 @@ describe("SkillTool", () => {
list: () => Effect.succeed(current),
}),
)
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const tool = SkillTool.layer.pipe(
Layer.provide(registry),
Layer.provide(permission),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(skills),
const skillToolLayer = AppNodeBuilder.build(
LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, SkillTool.node]),
[
[PermissionV2.node, permission],
[SkillV2.node, skills],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
],
)
const layer = Layer.mergeAll(permission, skills, registry, tool)
return yield* Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
@ -139,7 +141,7 @@ describe("SkillTool", () => {
call: { type: "tool-call", id: "call-flat-skill", name: "skill", input: { name: "public" } },
}),
).toEqual({ type: "text", value: SkillTool.toModelOutput(flat, []) })
}).pipe(Effect.provide(layer))
}).pipe(Effect.provide(skillToolLayer))
}),
),
),

View file

@ -92,19 +92,17 @@ const executionNode = makeGlobalNode({
})
const layer = AppNodeBuilder.build(
LayerNode.bind(
LayerNode.group([
Database.node,
EventV2.node,
Job.node,
ToolOutputStore.cleanupNode,
SessionV2.node,
PluginRuntime.providerNode,
LocationServiceMap.node,
]),
LayerNode.group([
Database.node,
EventV2.node,
Job.node,
ToolOutputStore.cleanupNode,
SessionV2.node,
SessionExecution.node,
executionNode,
),
PluginRuntime.providerNode,
LocationServiceMap.node,
]),
[[SessionExecution.node, executionNode]],
)
const it = testEffect(layer)

View file

@ -1,6 +1,8 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } 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 { PermissionV2 } from "@opencode-ai/core/permission"
import { Project } from "@opencode-ai/core/project"
@ -11,6 +13,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 +35,21 @@ 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 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,
TodoWriteTool.node,
]),
[
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
],
),
)
const setup = Effect.gen(function* () {

View file

@ -1,11 +1,15 @@
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 { 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 +39,14 @@ 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 toolLayer = (replacements: LayerNode.Replacements = []) =>
AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebFetchTool.node]), [
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
...replacements,
])
const it = testEffect(toolLayer([[LayerNodePlatform.httpClient, http]]))
const live = testEffect(toolLayer())
const reset = () => {
requests.length = 0

View file

@ -1,10 +1,14 @@
import { beforeEach, describe, expect, test } from "bun:test"
import { Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
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 +103,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 +123,17 @@ const websearchConfig = Layer.succeed(
},
}),
)
const websearch = WebSearchTool.layer.pipe(
Layer.provide(registry),
Layer.provide(permission),
Layer.provide(http),
Layer.provide(websearchConfig),
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearchTool.configNode, WebSearchTool.node]),
[
[PermissionV2.node, permission],
[LayerNodePlatform.httpClient, http],
[WebSearchTool.configNode, websearchConfig],
[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", () =>

View file

@ -4,6 +4,8 @@ import { fileURLToPath } from "url"
import { describe, expect, test } from "bun:test"
import { Effect, Layer } from "effect"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
@ -11,6 +13,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { WriteTool } from "@opencode-ai/core/tool/write"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
@ -55,25 +58,34 @@ const filesystem = Layer.effect(
Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeWithDirs(target, content, mode))),
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) => {
const activeLocation = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
)
const resolution = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
const mutation = FileMutation.layer.pipe(Layer.provide(filesystem))
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const write = WriteTool.layer.pipe(
Layer.provide(registry),
Layer.provide(permission),
Layer.provide(resolution),
Layer.provide(mutation),
)
return Effect.gen(function* () {
return yield* body(yield* ToolRegistry.Service)
}).pipe(Effect.provide(Layer.mergeAll(registry, resolution, mutation, write)))
}).pipe(
Effect.provide(
AppNodeBuilder.build(
LayerNode.group([
ToolRegistry.node,
ToolRegistry.toolsNode,
LocationMutation.node,
FileMutation.node,
WriteTool.node,
]),
[
[FSUtil.node, filesystem],
[Location.node, activeLocation],
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
],
),
),
)
}
const call = (input: typeof WriteTool.Input.Type, id = "call-write") => ({

View file

@ -3,9 +3,10 @@ import { spawn } from "child_process"
import fs from "fs/promises"
import path from "path"
import os from "os"
import { Cause, Effect, Exit, Layer } from "effect"
import { Cause, Effect, Exit } from "effect"
import { testEffect } from "../lib/effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Global } from "@opencode-ai/core/global"
import { Hash } from "@opencode-ai/core/util/hash"
@ -109,7 +110,7 @@ const testGlobal = Global.layerWith({
log: os.tmpdir(),
})
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(FSUtil.defaultLayer))
const testLayer = AppNodeBuilder.build(EffectFlock.node, [[Global.node, testGlobal]])
// ---------------------------------------------------------------------------
// Tests