layer-node-tree.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import { expect, test } from "bun:test"
  2. import { Context, Effect, Layer } from "effect"
  3. import { LayerNode } from "@opencode-ai/core/effect/layer-node"
  4. import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node-tree"
  5. import { ScopedNode } from "@opencode-ai/core/effect/scoped-node"
  6. import { ScopedNodeBuild } from "@opencode-ai/core/effect/scoped-node-build"
  7. import { LocationServiceMap } from "@opencode-ai/core/location-layer"
  8. import { Location } from "@opencode-ai/core/location"
  9. class Database extends Context.Service<Database, { readonly name: string }>()("test/GraphDatabase") {}
  10. class Users extends Context.Service<Users, { readonly list: Effect.Effect<string[]> }>()("test/GraphUsers") {}
  11. class App extends Context.Service<App, { readonly run: Effect.Effect<string[]> }>()("test/GraphApp") {}
  12. test("separates, hoists, and compiles tier graphs", async () => {
  13. const tiers = LayerNode.tiers(["location", "global"])
  14. const global = tiers.make("global")
  15. const location = tiers.make("location")
  16. const database = global({
  17. service: Database,
  18. layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
  19. deps: [],
  20. })
  21. const users = location({
  22. service: Users,
  23. layer: Layer.effect(
  24. Users,
  25. Effect.gen(function* () {
  26. const db = yield* Database
  27. return Users.of({ list: Effect.succeed([db.name]) })
  28. }),
  29. ),
  30. deps: [database],
  31. })
  32. const app = location({
  33. service: App,
  34. layer: Layer.effect(
  35. App,
  36. Effect.gen(function* () {
  37. const service = yield* Users
  38. return App.of({ run: service.list })
  39. }),
  40. ),
  41. deps: [users],
  42. })
  43. const separated = LayerNodeTree.separate(LayerNode.group([app]), tiers)
  44. expect(separated.location.dependencies).toEqual([app])
  45. expect(separated.global.dependencies).toEqual([])
  46. const locationResult = LayerNodeTree.hoist(separated.location, tiers.values.location, tiers)
  47. const globalResult = LayerNodeTree.hoist(
  48. { ...separated.global, dependencies: [...separated.global.dependencies, ...locationResult.hoisted.dependencies] },
  49. tiers.values.global,
  50. tiers,
  51. )
  52. expect(locationResult.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
  53. kind: "group",
  54. dependencies: [],
  55. })
  56. expect(locationResult.hoisted.dependencies).toEqual([database])
  57. const layer = LayerNodeTree.compile(locationResult.node).pipe(
  58. Layer.provide(LayerNodeTree.compile(globalResult.node)),
  59. ) as unknown as Layer.Layer<App>
  60. const program = Effect.gen(function* () {
  61. return yield* (yield* App).run
  62. }).pipe(Effect.provide(layer))
  63. expect(await Effect.runPromise(program)).toEqual(["Alice"])
  64. })
  65. test("rejects conflicting hoisted implementations", () => {
  66. const tiers = LayerNode.tiers(["location", "global"])
  67. const global = tiers.make("global")
  68. const location = tiers.make("location")
  69. const first = global({ service: Database, layer: Layer.succeed(Database, Database.of({ name: "first" })), deps: [] })
  70. const second = global({ service: Database, layer: Layer.succeed(Database, Database.of({ name: "second" })), deps: [] })
  71. const left = location({ service: Users, layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))), deps: [first] })
  72. const right = location({ service: App, layer: Layer.effect(App, Effect.as(Database, App.of({ run: Effect.succeed([]) }))), deps: [second] })
  73. const separated = LayerNodeTree.separate(LayerNode.group([left, right]), tiers)
  74. expect(() => LayerNodeTree.hoist(separated.location, tiers.values.location, tiers)).toThrow(
  75. "Tier location has conflicting implementations for test/GraphDatabase",
  76. )
  77. })
  78. test("treats dependency groups as transparent while hoisting", () => {
  79. const tiers = LayerNode.tiers(["location", "global"])
  80. const global = tiers.make("global")
  81. const location = tiers.make("location")
  82. const database = global({
  83. service: Database,
  84. layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
  85. deps: [],
  86. })
  87. const users = location({
  88. service: Users,
  89. layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
  90. deps: [LayerNode.group([database])],
  91. })
  92. const separated = LayerNodeTree.separate(LayerNode.group([users]), tiers)
  93. const result = LayerNodeTree.hoist(separated.location, tiers.values.location, tiers)
  94. expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
  95. kind: "group",
  96. dependencies: [],
  97. })
  98. })
  99. test("builds the scoped location and global trees", async () => {
  100. const database = ScopedNode.makeGlobalNode({
  101. service: Database,
  102. layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
  103. deps: [],
  104. })
  105. const app = ScopedNode.makeLocationNode({
  106. service: App,
  107. layer: Layer.effect(App, Effect.map(Database, (db) => App.of({ run: Effect.succeed([db.name]) }))),
  108. deps: [database],
  109. })
  110. const serviceLayer = ScopedNodeBuild.build(LayerNode.group([app]))
  111. const layer = LocationServiceMap.get({ directory: "/tmp" } as Location.Ref).pipe(
  112. Layer.provide(serviceLayer),
  113. ) as unknown as Layer.Layer<App>
  114. const program = Effect.gen(function* () {
  115. return yield* (yield* App).run
  116. }).pipe(Effect.provide(layer))
  117. expect(await Effect.runPromise(program)).toEqual(["Alice"])
  118. })