index.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import { describe, expect, spyOn } from "bun:test"
  2. import path from "path"
  3. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  4. import { Deferred, Effect, Layer } from "effect"
  5. import { EventV2Bridge } from "@/event-v2-bridge"
  6. import { Config } from "@/config/config"
  7. import { RuntimeFlags } from "@/effect/runtime-flags"
  8. import { LSP } from "@/lsp/lsp"
  9. import * as LSPServer from "@/lsp/server"
  10. import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
  11. import { TestInstance } from "../fixture/fixture"
  12. import { awaitWithTimeout, testEffect } from "../lib/effect"
  13. const lspLayer = (flags: Parameters<typeof RuntimeFlags.layer>[0] = {}) =>
  14. LayerNode.compile(LayerNode.group([LSP.node, Config.node, RuntimeFlags.node, EventV2Bridge.node]), [
  15. [RuntimeFlags.node, RuntimeFlags.layer(flags)],
  16. ])
  17. const it = testEffect(Layer.mergeAll(lspLayer(), LayerNode.compile(CrossSpawnSpawner.node)))
  18. const experimentalTyIt = testEffect(
  19. Layer.mergeAll(lspLayer({ experimentalLspTy: true }), LayerNode.compile(CrossSpawnSpawner.node)),
  20. )
  21. const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
  22. const disabledDownloadIt = testEffect(
  23. Layer.mergeAll(lspLayer({ disableLspDownload: true }), LayerNode.compile(CrossSpawnSpawner.node)),
  24. )
  25. describe("lsp.spawn", () => {
  26. it.instance(
  27. "does not spawn builtin LSP for files outside instance",
  28. () =>
  29. LSP.Service.use((lsp) =>
  30. Effect.gen(function* () {
  31. const dir = (yield* TestInstance).directory
  32. const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
  33. try {
  34. yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
  35. yield* lsp.hover({
  36. file: path.join(dir, "..", "hover.ts"),
  37. line: 0,
  38. character: 0,
  39. })
  40. expect(spy).toHaveBeenCalledTimes(0)
  41. } finally {
  42. spy.mockRestore()
  43. }
  44. }),
  45. ),
  46. { config: { lsp: true } },
  47. )
  48. it.instance("does not spawn builtin LSP for files inside instance when LSP is unset", () =>
  49. LSP.Service.use((lsp) =>
  50. Effect.gen(function* () {
  51. const dir = (yield* TestInstance).directory
  52. const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
  53. try {
  54. yield* lsp.hover({
  55. file: path.join(dir, "src", "inside.ts"),
  56. line: 0,
  57. character: 0,
  58. })
  59. expect(spy).toHaveBeenCalledTimes(0)
  60. } finally {
  61. spy.mockRestore()
  62. }
  63. }),
  64. ),
  65. )
  66. it.instance(
  67. "would spawn builtin LSP for files inside instance when lsp is true",
  68. () =>
  69. LSP.Service.use((lsp) =>
  70. Effect.gen(function* () {
  71. const dir = (yield* TestInstance).directory
  72. const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
  73. try {
  74. yield* lsp.hover({
  75. file: path.join(dir, "src", "inside.ts"),
  76. line: 0,
  77. character: 0,
  78. })
  79. expect(spy).toHaveBeenCalledTimes(1)
  80. } finally {
  81. spy.mockRestore()
  82. }
  83. }),
  84. ),
  85. { config: { lsp: true } },
  86. )
  87. it.instance(
  88. "publishes lsp.updated after custom LSP initialization",
  89. () =>
  90. Effect.gen(function* () {
  91. const dir = (yield* TestInstance).directory
  92. const lsp = yield* LSP.Service
  93. const updated = yield* Deferred.make<void>()
  94. const events = yield* EventV2Bridge.Service
  95. const unsubscribe = yield* events.listen((event) => {
  96. if (event.type === LSP.Event.Updated.type) Deferred.doneUnsafe(updated, Effect.void)
  97. return Effect.void
  98. })
  99. yield* Effect.addFinalizer(() => unsubscribe)
  100. const file = path.join(dir, "sample.repro")
  101. yield* Effect.promise(() => Bun.write(file, "sample\n"))
  102. yield* lsp.touchFile(file)
  103. yield* awaitWithTimeout(Deferred.await(updated), "lsp.updated event was not published")
  104. }),
  105. {
  106. config: {
  107. lsp: {
  108. fake: {
  109. command: [process.execPath, fakeServerPath],
  110. extensions: [".repro"],
  111. },
  112. },
  113. },
  114. },
  115. )
  116. it.instance(
  117. "would spawn builtin LSP for files inside instance when config object is provided",
  118. () =>
  119. LSP.Service.use((lsp) =>
  120. Effect.gen(function* () {
  121. const dir = (yield* TestInstance).directory
  122. const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
  123. try {
  124. yield* lsp.hover({
  125. file: path.join(dir, "src", "inside.ts"),
  126. line: 0,
  127. character: 0,
  128. })
  129. expect(spy).toHaveBeenCalledTimes(1)
  130. } finally {
  131. spy.mockRestore()
  132. }
  133. }),
  134. ),
  135. {
  136. config: {
  137. lsp: {
  138. eslint: { disabled: true },
  139. },
  140. },
  141. },
  142. )
  143. it.instance(
  144. "uses pyright instead of ty by default",
  145. () =>
  146. LSP.Service.use((lsp) =>
  147. Effect.gen(function* () {
  148. const dir = (yield* TestInstance).directory
  149. const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
  150. const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
  151. try {
  152. yield* lsp.hover({
  153. file: path.join(dir, "src", "inside.py"),
  154. line: 0,
  155. character: 0,
  156. })
  157. expect(ty).toHaveBeenCalledTimes(0)
  158. expect(pyright).toHaveBeenCalledTimes(1)
  159. } finally {
  160. ty.mockRestore()
  161. pyright.mockRestore()
  162. }
  163. }),
  164. ),
  165. { config: { lsp: true } },
  166. )
  167. experimentalTyIt.instance(
  168. "uses ty instead of pyright when experimentalLspTy is enabled",
  169. () =>
  170. LSP.Service.use((lsp) =>
  171. Effect.gen(function* () {
  172. const dir = (yield* TestInstance).directory
  173. const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
  174. const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
  175. try {
  176. yield* lsp.hover({
  177. file: path.join(dir, "src", "inside.py"),
  178. line: 0,
  179. character: 0,
  180. })
  181. expect(ty).toHaveBeenCalledTimes(1)
  182. expect(pyright).toHaveBeenCalledTimes(0)
  183. } finally {
  184. ty.mockRestore()
  185. pyright.mockRestore()
  186. }
  187. }),
  188. ),
  189. { config: { lsp: true } },
  190. )
  191. disabledDownloadIt.instance(
  192. "passes disableLspDownload to builtin LSP spawn",
  193. () =>
  194. LSP.Service.use((lsp) =>
  195. Effect.gen(function* () {
  196. const dir = (yield* TestInstance).directory
  197. const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
  198. try {
  199. yield* lsp.hover({
  200. file: path.join(dir, "src", "inside.py"),
  201. line: 0,
  202. character: 0,
  203. })
  204. expect(pyright).toHaveBeenCalledTimes(1)
  205. expect(pyright.mock.calls[0]?.[2]).toMatchObject({ disableLspDownload: true })
  206. } finally {
  207. pyright.mockRestore()
  208. }
  209. }),
  210. ),
  211. { config: { lsp: true } },
  212. )
  213. })