node-build.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { describe, expect, test } from "bun:test"
  2. import { Context, Effect, Layer } from "effect"
  3. import { LayerNode } from "@opencode-ai/core/effect/layer-node"
  4. import { Node } from "@opencode-ai/core/effect/node"
  5. import { NodeBuild } from "@opencode-ai/core/effect/node-build"
  6. import { Location } from "@opencode-ai/core/location"
  7. import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
  8. import { Project } from "@opencode-ai/core/project"
  9. import { AbsolutePath } from "@opencode-ai/core/schema"
  10. import { tmpdir } from "../../fixture/tmpdir"
  11. class Value extends Context.Service<Value, { readonly value: string }>()("test/TagValue") {}
  12. class Result extends Context.Service<Result, { readonly value: string }>()("test/TagResult") {}
  13. class Left extends Context.Service<Left, { readonly value: string }>()("test/TagLeft") {}
  14. class Right extends Context.Service<Right, { readonly value: string }>()("test/TagRight") {}
  15. class Last extends Context.Service<Last, { readonly value: string }>()("test/TagLast") {}
  16. describe("node build", () => {
  17. test("shares top-level project with location services", async () => {
  18. await using tmp = await tmpdir()
  19. let acquisitions = 0
  20. const projectLayer = Layer.effect(
  21. Project.Service,
  22. Effect.sync(() => {
  23. acquisitions++
  24. return Project.Service.of({
  25. directories: () => Effect.succeed([]),
  26. resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }),
  27. commit: () => Effect.void,
  28. })
  29. }),
  30. )
  31. const layer = NodeBuild.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
  32. LayerNode.replace(Project.layer, projectLayer),
  33. ])
  34. const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
  35. const program = Effect.gen(function* () {
  36. yield* Project.Service
  37. const locations = yield* LocationServiceMap.Service
  38. return yield* Location.Service.pipe(Effect.provide(locations.get(ref)))
  39. }).pipe(Effect.provide(layer))
  40. expect((await Effect.runPromise(program)).directory).toBe(ref.directory)
  41. expect(acquisitions).toBe(1)
  42. })
  43. test("returns a composed application layer", async () => {
  44. const value = Node.makeGlobalNode({
  45. service: Value,
  46. layer: Layer.succeed(Value, Value.of({ value: "value" })),
  47. deps: [],
  48. })
  49. const result = Node.makeGlobalNode({
  50. service: Result,
  51. layer: Layer.effect(
  52. Result,
  53. Effect.gen(function* () {
  54. return Result.of({ value: (yield* Value).value })
  55. }),
  56. ),
  57. deps: [value],
  58. })
  59. const serviceLayer = NodeBuild.build(LayerNode.group([result]))
  60. const program = Effect.gen(function* () {
  61. return (yield* Result).value
  62. }).pipe(Effect.provide(serviceLayer))
  63. expect(await Effect.runPromise(program)).toBe("value")
  64. })
  65. test("rebinds same-tag providers without reacquiring them", async () => {
  66. let firstAcquisitions = 0
  67. const tags = LayerNode.tags({ global: [] })
  68. const global = tags.make("global")
  69. const first = global({
  70. service: Value,
  71. layer: Layer.effect(
  72. Value,
  73. Effect.sync(() => {
  74. firstAcquisitions++
  75. return Value.of({ value: "first" })
  76. }),
  77. ),
  78. deps: [],
  79. })
  80. const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
  81. const left = global({
  82. service: Left,
  83. layer: Layer.effect(
  84. Left,
  85. Effect.map(Value, (value) => Left.of({ value: value.value })),
  86. ),
  87. deps: [first],
  88. })
  89. const right = global({
  90. service: Right,
  91. layer: Layer.effect(
  92. Right,
  93. Effect.map(Value, (value) => Right.of({ value: value.value })),
  94. ),
  95. deps: [second],
  96. })
  97. const last = global({
  98. service: Last,
  99. layer: Layer.effect(
  100. Last,
  101. Effect.map(Value, (value) => Last.of({ value: value.value })),
  102. ),
  103. deps: [first],
  104. })
  105. const layer = NodeBuild.build(LayerNode.group([left, right, last])) as Layer.Layer<Left | Right | Last>
  106. const values = Effect.gen(function* () {
  107. return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
  108. }).pipe(Effect.provide(layer))
  109. expect(await Effect.runPromise(values)).toEqual(["first", "second", "first"])
  110. expect(firstAcquisitions).toBe(1)
  111. })
  112. })