lsp.test.ts 5.6 KB

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