node-build.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { describe, expect, test } from "bun:test"
  2. import { Context, Effect, Layer, LayerMap, Option } from "effect"
  3. import { Node } from "@opencode-ai/util/effect/app-node"
  4. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  5. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  6. import { Location } from "@opencode-ai/core/location"
  7. import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
  8. import type { LocationError, LocationServices } from "@opencode-ai/core/location-services"
  9. import { Project } from "@opencode-ai/core/project"
  10. import { AbsolutePath } from "@opencode-ai/core/schema"
  11. import { tmpdir } from "../../fixture/tmpdir"
  12. class Value extends Context.Service<Value, { readonly value: string }>()("test/TagValue") {}
  13. class Result extends Context.Service<Result, { readonly value: string }>()("test/TagResult") {}
  14. class CycleA extends Context.Service<CycleA, {}>()("test/NodeBuildA") {}
  15. class CycleB extends Context.Service<CycleB, { readonly directory: AbsolutePath }>()("test/NodeBuildB") {}
  16. describe("node build", () => {
  17. test("does not build a location service map when the graph does not require it", async () => {
  18. const result = Node.makeGlobalNode({
  19. service: Result,
  20. layer: Layer.succeed(Result, Result.of({ value: "plain" })),
  21. deps: [],
  22. })
  23. const layer = AppNodeBuilder.build(result)
  24. const program = Effect.gen(function* () {
  25. expect(Option.isNone(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true)
  26. return (yield* Result).value
  27. }).pipe(Effect.provide(layer))
  28. expect(await Effect.runPromise(program)).toBe("plain")
  29. })
  30. test("detects cycles through a replaced location service map", async () => {
  31. const a = Node.makeGlobalNode({
  32. service: CycleA,
  33. layer: Layer.effect(CycleA, Effect.as(LocationServiceMap.Service, CycleA.of({}))),
  34. deps: [LocationServiceMap.node],
  35. })
  36. const b = Node.makeGlobalNode({
  37. service: CycleB,
  38. layer: Layer.effect(
  39. CycleB,
  40. Effect.map(CycleA, () => CycleB.of({ directory: AbsolutePath.make(process.cwd()) })),
  41. ),
  42. deps: [a],
  43. })
  44. const mapLayer = Layer.effect(
  45. LocationServiceMap.Service,
  46. Effect.gen(function* () {
  47. const service = yield* CycleB
  48. return yield* LayerMap.make(
  49. (ref: Location.Ref) =>
  50. Layer.succeed(
  51. Location.Service,
  52. Location.Service.of({
  53. directory: ref.directory,
  54. workspaceID: ref.workspaceID,
  55. project: { id: Project.ID.global, directory: service.directory, canonical: service.directory },
  56. }),
  57. ),
  58. { idleTimeToLive: "1 minute" },
  59. )
  60. }) as unknown as Effect.Effect<LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>, never, CycleB>,
  61. )
  62. const map = Node.makeGlobalNode({ service: LocationServiceMap.Service, layer: mapLayer, deps: [b] })
  63. expect(() => AppNodeBuilder.build(LayerNode.group([a]), [[LocationServiceMap.node, map]])).toThrow(
  64. "Cycle detected in layer tree",
  65. )
  66. })
  67. test("shares top-level project with location services", async () => {
  68. await using tmp = await tmpdir()
  69. let acquisitions = 0
  70. const projectLayer = Layer.effect(
  71. Project.Service,
  72. Effect.sync(() => {
  73. acquisitions++
  74. return Project.Service.of({
  75. list: () => Effect.succeed([]),
  76. directories: () => Effect.succeed([]),
  77. resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
  78. commit: () => Effect.void,
  79. })
  80. }),
  81. )
  82. const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
  83. const layer = AppNodeBuilder.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
  84. [Project.node, projectLayer],
  85. ])
  86. const program = Effect.gen(function* () {
  87. yield* Project.Service
  88. const locations = yield* LocationServiceMap.Service
  89. expect(Option.isSome(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true)
  90. return yield* Location.Service.pipe(Effect.provide(locations.get(ref)))
  91. }).pipe(Effect.provide(layer))
  92. expect((await Effect.runPromise(program)).directory).toBe(ref.directory)
  93. expect(acquisitions).toBe(1)
  94. })
  95. test("returns a composed application layer", async () => {
  96. const value = Node.makeGlobalNode({
  97. service: Value,
  98. layer: Layer.succeed(Value, Value.of({ value: "value" })),
  99. deps: [],
  100. })
  101. const result = Node.makeGlobalNode({
  102. service: Result,
  103. layer: Layer.effect(
  104. Result,
  105. Effect.gen(function* () {
  106. return Result.of({ value: (yield* Value).value })
  107. }),
  108. ),
  109. deps: [value],
  110. })
  111. const serviceLayer = AppNodeBuilder.build(result)
  112. const program = Effect.gen(function* () {
  113. expect(Option.isNone(yield* Effect.serviceOption(LocationServiceMap.Service))).toBe(true)
  114. return (yield* Result).value
  115. }).pipe(Effect.provide(serviceLayer))
  116. expect(await Effect.runPromise(program)).toBe("value")
  117. })
  118. })