import { describe, expect, test } from "bun:test" import { buildNode } from "@/effect/build-node" import { Context, Effect, Layer } from "effect" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree" class Value extends Context.Service()("test/LayerNodeValue") {} class Greeting extends Context.Service()("test/LayerNodeGreeting") {} class Left extends Context.Service()("test/LayerNodeLeft") {} class Right extends Context.Service()("test/LayerNodeRight") {} const tiers = LayerNode.tiers(["app"]) const make = tiers.make("app") const build = (root: LayerNode.Node, replacements?: readonly LayerNode.Replacement[]) => LayerNodeTree.compile( LayerNodeTree.separate(root, tiers).app, new Map(replacements?.map((item) => [item.source, item.replacement])), ) as Layer.Layer const valueLayer = Layer.succeed(Value, Value.of({ value: "production" })) const greetingLayer = Layer.effect( Greeting, Effect.map(Value, (value) => Greeting.of({ value: `hello ${value.value}` })), ) const value = make({ service: Value, layer: valueLayer, deps: [] }) const greeting = make({ service: Greeting, layer: greetingLayer, deps: [value] }) describe("layer node", () => { test("builds an untiered graph", async () => { const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] }) const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] }) const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(buildNode(LayerNode.group([greeting])))) expect(await Effect.runPromise(program)).toBe("hello production") }) test("builds a dependency graph", async () => { const program = Effect.map(Greeting, (item) => item.value).pipe( Effect.provide(build(LayerNode.group([greeting]))), ) expect(await Effect.runPromise(program)).toBe("hello production") }) test("exposes roots but hides transitive dependencies", () => { const layer = build(LayerNode.group([greeting])) const check: Layer.Layer = layer void check }) test("preserves branch-specific implementations across roots", async () => { const firstValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] }) const secondValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] }) const leftLayer = Layer.effect(Left, Effect.map(Value, (item) => Left.of({ value: item.value }))) const rightLayer = Layer.effect(Right, Effect.map(Value, (item) => Right.of({ value: item.value }))) const left = make({ service: Left, layer: leftLayer, deps: [firstValue] }) const right = make({ service: Right, layer: rightLayer, deps: [secondValue] }) const layer = build(LayerNode.group([left, right])) const program = Effect.gen(function* () { return [(yield* Left).value, (yield* Right).value] }).pipe(Effect.provide(layer)) expect(await Effect.runPromise(program)).toEqual(["first", "second"]) }) test("requires unbound nodes to be bound before compilation", async () => { const unbound = LayerNode.unbound(Value, tiers.values.app) const greeting = make({ service: Greeting, layer: greetingLayer, deps: [unbound] }) const tree = LayerNodeTree.separate(LayerNode.group([greeting]), tiers).app expect(() => LayerNodeTree.compile(tree)).toThrow("Unbound layer node: test/LayerNodeValue") const bound = LayerNodeTree.bind(tree, unbound, value) const layer = LayerNodeTree.compile(bound) as Layer.Layer 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 () => { 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)]), ), ) expect(await Effect.runPromise(program)).toBe("hello simulation") }) test("replaces every use of the same layer", async () => { const leftLayer = Layer.effect( Left, Effect.map(Value, (item) => Left.of({ value: item.value })), ) const rightLayer = Layer.effect( Right, Effect.map(Value, (item) => Right.of({ value: item.value })), ) 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 program = Effect.gen(function* () { return [(yield* Left).value, (yield* Right).value] }).pipe(Effect.provide(layer)) expect(await Effect.runPromise(program)).toEqual(["replaced", "replaced"]) }) test("does not acquire an unused replacement", async () => { let acquisitions = 0 const other = Layer.succeed(Value, Value.of({ value: "other" })) const replacement = Layer.effect( Value, Effect.sync(() => { acquisitions++ return Value.of({ value: "replacement" }) }), ) await Effect.runPromise( Effect.map(Greeting, (item) => item.value).pipe( Effect.provide( build(LayerNode.group([greeting]), [LayerNode.replace(other, replacement)]), ), ), ) expect(acquisitions).toBe(0) }) })