| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- import { describe, expect, test } from "bun:test"
- import { Context, Effect, Layer } from "effect"
- import { LayerNode } from "@opencode-ai/util/effect/layer-node"
- class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
- class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
- class Left extends Context.Service<Left, { readonly value: string }>()("test/LayerNodeLeft") {}
- class Right extends Context.Service<Right, { readonly value: string }>()("test/LayerNodeRight") {}
- class Database extends Context.Service<Database, { readonly name: string }>()("test/GraphDatabase") {}
- class Users extends Context.Service<Users, { readonly list: Effect.Effect<string[]> }>()("test/GraphUsers") {}
- class App extends Context.Service<App, { readonly run: Effect.Effect<string[]> }>()("test/GraphApp") {}
- 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, replacements) as Layer.Layer<A, E>
- 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 untagged 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(LayerNode.compile(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<Greeting> = 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 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 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 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]), [[value, 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]), [[value, 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 = make({ service: Left, layer: Layer.succeed(Left, Left.of({ value: "other" })), deps: [] })
- const replacement = Layer.effect(
- Left,
- Effect.sync(() => {
- acquisitions++
- return Left.of({ value: "replacement" })
- }),
- )
- await Effect.runPromise(
- Effect.map(Greeting, (item) => item.value).pipe(
- 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")
- const location = tags.make("location")
- const database = global({
- service: Database,
- layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
- deps: [],
- })
- const users = location({
- service: Users,
- layer: Layer.effect(
- Users,
- Effect.gen(function* () {
- const db = yield* Database
- return Users.of({ list: Effect.succeed([db.name]) })
- }),
- ),
- deps: [database],
- })
- const app = location({
- service: App,
- layer: Layer.effect(
- App,
- Effect.gen(function* () {
- const service = yield* Users
- return App.of({ run: service.list })
- }),
- ),
- deps: [users],
- })
- const result = LayerNode.hoist(LayerNode.group([app]), tags.values.global)
- expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
- kind: "group",
- dependencies: [],
- })
- expect(result.hoisted.dependencies).toEqual([database])
- const layer = LayerNode.compile(result.node).pipe(
- Layer.provide(LayerNode.compile(result.hoisted)),
- ) as unknown as Layer.Layer<App>
- const program = Effect.gen(function* () {
- return yield* (yield* App).run
- }).pipe(Effect.provide(layer))
- expect(await Effect.runPromise(program)).toEqual(["Alice"])
- })
- test("rejects conflicting hoisted implementations", () => {
- const tags = LayerNode.tags({ location: ["global"], global: [] })
- const global = tags.make("global")
- const location = tags.make("location")
- const first = global({
- service: Database,
- layer: Layer.succeed(Database, Database.of({ name: "first" })),
- deps: [],
- })
- const second = global({
- service: Database,
- layer: Layer.succeed(Database, Database.of({ name: "second" })),
- deps: [],
- })
- const left = location({
- service: Users,
- layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
- deps: [first],
- })
- const right = location({
- service: App,
- layer: Layer.effect(App, Effect.as(Database, App.of({ run: Effect.succeed([]) }))),
- deps: [second],
- })
- expect(() => LayerNode.hoist(LayerNode.group([left, right]), tags.values.global)).toThrow(
- "Tag global has conflicting implementations for test/GraphDatabase",
- )
- })
- test("treats dependency groups as transparent while hoisting", () => {
- const tags = LayerNode.tags({ location: ["global"], global: [] })
- const global = tags.make("global")
- const location = tags.make("location")
- const database = global({
- service: Database,
- layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
- deps: [],
- })
- const users = location({
- service: Users,
- layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
- deps: [LayerNode.group([database])],
- })
- const result = LayerNode.hoist(LayerNode.group([users]), tags.values.global)
- expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
- kind: "group",
- dependencies: [],
- })
- })
- })
|