import { describe, expect, test } from "bun:test" import { Context, Effect, Layer, LayerMap, Option } from "effect" 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" import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" import { tmpdir } from "../../fixture/tmpdir" class Value extends Context.Service()("test/TagValue") {} class Result extends Context.Service()("test/TagResult") {} class CycleA extends Context.Service()("test/NodeBuildA") {} class CycleB extends Context.Service()("test/NodeBuildB") {} describe("node build", () => { test("does not build a location service map when the graph does not require it", async () => { const result = Node.makeGlobalNode({ service: Result, layer: Layer.succeed(Result, Result.of({ value: "plain" })), deps: [], }) const layer = AppNodeBuilder.build(result) const program = Effect.gen(function* () { expect(Option.isNone(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true) return (yield* Result).value }).pipe(Effect.provide(layer)) expect(await Effect.runPromise(program)).toBe("plain") }) 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({}))), deps: [LocationServiceMap.node], }) const b = Node.makeGlobalNode({ service: CycleB, layer: Layer.effect( CycleB, Effect.map(CycleA, () => CycleB.of({ directory: AbsolutePath.make(process.cwd()) })), ), deps: [a], }) 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, never, CycleB>, ) const map = Node.makeGlobalNode({ service: LocationServiceMap.Service, layer: mapLayer, deps: [b] }) expect(() => AppNodeBuilder.build(LayerNode.group([a]), [[LocationServiceMap.node, map]])).toThrow( "Cycle detected in layer tree", ) }) test("shares top-level project with location services", async () => { await using tmp = await tmpdir() let acquisitions = 0 const projectLayer = Layer.effect( Project.Service, Effect.sync(() => { acquisitions++ return Project.Service.of({ directories: () => Effect.succeed([]), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), commit: () => Effect.void, }) }), ) 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 expect(Option.isSome(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true) return yield* Location.Service.pipe(Effect.provide(locations.get(ref))) }).pipe(Effect.provide(layer)) expect((await Effect.runPromise(program)).directory).toBe(ref.directory) expect(acquisitions).toBe(1) }) test("returns a composed application layer", async () => { const value = Node.makeGlobalNode({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "value" })), deps: [], }) const result = Node.makeGlobalNode({ service: Result, layer: Layer.effect( Result, Effect.gen(function* () { return Result.of({ value: (yield* Value).value }) }), ), deps: [value], }) const serviceLayer = AppNodeBuilder.build(result) const program = Effect.gen(function* () { expect(Option.isNone(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true) return (yield* Result).value }).pipe(Effect.provide(serviceLayer)) expect(await Effect.runPromise(program)).toBe("value") }) })