lsp.test.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { PermissionV1 } from "@kirincode-ai/core/v1/permission"
  2. import { afterEach, describe, expect } from "bun:test"
  3. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  4. import { Effect, Layer } from "effect"
  5. import path from "path"
  6. import { Agent } from "../../src/agent/agent"
  7. import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
  8. import { FSUtil } from "@kirincode-ai/core/fs-util"
  9. import { LSP } from "@/lsp/lsp"
  10. import { Permission } from "../../src/permission"
  11. import { MessageID, SessionID } from "../../src/session/schema"
  12. import { Tool } from "@/tool/tool"
  13. import { Truncate } from "@/tool/truncate"
  14. import { LspTool } from "../../src/tool/lsp"
  15. import { disposeAllInstances, TestInstance } from "../fixture/fixture"
  16. import { testEffect } from "../lib/effect"
  17. afterEach(async () => {
  18. await disposeAllInstances()
  19. })
  20. const ctx = {
  21. sessionID: SessionID.make("ses_test"),
  22. messageID: MessageID.make("msg_test"),
  23. callID: "",
  24. agent: "build",
  25. abort: AbortSignal.any([]),
  26. messages: [],
  27. metadata: () => Effect.void,
  28. ask: () => Effect.void,
  29. }
  30. const workspaceSymbolQueries: string[] = []
  31. const lsp = Layer.succeed(
  32. LSP.Service,
  33. LSP.Service.of({
  34. init: () => Effect.void,
  35. status: () => Effect.succeed([]),
  36. hasClients: () => Effect.succeed(true),
  37. touchFile: () => Effect.void,
  38. diagnostics: () => Effect.succeed({}),
  39. hover: () => Effect.succeed([]),
  40. definition: () => Effect.succeed([]),
  41. references: () => Effect.succeed([]),
  42. implementation: () => Effect.succeed([]),
  43. documentSymbol: () => Effect.succeed([]),
  44. workspaceSymbol: (query) =>
  45. Effect.sync(() => {
  46. workspaceSymbolQueries.push(query)
  47. return []
  48. }),
  49. prepareCallHierarchy: () => Effect.succeed([]),
  50. incomingCalls: () => Effect.succeed([]),
  51. outgoingCalls: () => Effect.succeed([]),
  52. }),
  53. )
  54. const it = testEffect(
  55. LayerNode.compile(LayerNode.group([Agent.node, FSUtil.node, CrossSpawnSpawner.node, Truncate.node, LSP.node]), [
  56. [LSP.node, lsp],
  57. ]),
  58. )
  59. const init = Effect.fn("LspToolTest.init")(function* () {
  60. const info = yield* LspTool
  61. return yield* info.init()
  62. })
  63. const run = Effect.fn("LspToolTest.run")(function* (
  64. args: Tool.InferParameters<typeof LspTool>,
  65. next: Tool.Context = ctx,
  66. ) {
  67. const tool = yield* init()
  68. return yield* tool.execute(args, next)
  69. })
  70. const put = Effect.fn("LspToolTest.put")(function* (file: string) {
  71. const fs = yield* FSUtil.Service
  72. yield* fs.writeWithDirs(file, "export const x = 1\n")
  73. })
  74. const asks = () => {
  75. const items: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
  76. return {
  77. items,
  78. next: {
  79. ...ctx,
  80. ask: (req: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">) =>
  81. Effect.sync(() => {
  82. items.push(req)
  83. }),
  84. },
  85. }
  86. }
  87. describe("tool.lsp", () => {
  88. describe("permission metadata", () => {
  89. it.instance(
  90. "keeps cursor details for position-based operations",
  91. () =>
  92. Effect.gen(function* () {
  93. const dir = (yield* TestInstance).directory
  94. const file = path.join(dir, "test.ts")
  95. yield* put(file)
  96. const { items, next } = asks()
  97. const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next)
  98. const req = items.find((item) => item.permission === "lsp")
  99. expect(req).toBeDefined()
  100. expect(req!.metadata).toEqual({
  101. operation: "goToDefinition",
  102. filePath: file,
  103. line: 3,
  104. character: 7,
  105. })
  106. expect(result.title).toBe("goToDefinition test.ts:3:7")
  107. }),
  108. { git: true },
  109. )
  110. it.instance(
  111. "omits cursor details for documentSymbol",
  112. () =>
  113. Effect.gen(function* () {
  114. const dir = (yield* TestInstance).directory
  115. const file = path.join(dir, "test.ts")
  116. yield* put(file)
  117. const { items, next } = asks()
  118. const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next)
  119. const req = items.find((item) => item.permission === "lsp")
  120. expect(req).toBeDefined()
  121. expect(req!.metadata).toEqual({
  122. operation: "documentSymbol",
  123. filePath: file,
  124. })
  125. expect(result.title).toBe("documentSymbol test.ts")
  126. }),
  127. { git: true },
  128. )
  129. it.instance(
  130. "omits file and cursor details for workspaceSymbol",
  131. () =>
  132. Effect.gen(function* () {
  133. const dir = (yield* TestInstance).directory
  134. workspaceSymbolQueries.length = 0
  135. const file = path.join(dir, "test.ts")
  136. yield* put(file)
  137. const { items, next } = asks()
  138. const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next)
  139. const req = items.find((item) => item.permission === "lsp")
  140. expect(req).toBeDefined()
  141. expect(req!.metadata).toEqual({
  142. operation: "workspaceSymbol",
  143. })
  144. expect(result.title).toBe("workspaceSymbol")
  145. }),
  146. { git: true },
  147. )
  148. it.instance(
  149. "passes workspaceSymbol query to LSP",
  150. () =>
  151. Effect.gen(function* () {
  152. const dir = (yield* TestInstance).directory
  153. workspaceSymbolQueries.length = 0
  154. const file = path.join(dir, "test.ts")
  155. yield* put(file)
  156. yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7, query: "TestSymbol" })
  157. yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 })
  158. expect(workspaceSymbolQueries).toEqual(["TestSymbol", ""])
  159. }),
  160. { git: true },
  161. )
  162. })
  163. })