lifecycle.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
  2. import path from "path"
  3. import { Effect, Layer } from "effect"
  4. import { LSP } from "../../src/lsp"
  5. import { LSPServer } from "../../src/lsp"
  6. import { CrossSpawnSpawner } from "@opencode-ai/core/effect/cross-spawn-spawner"
  7. import { provideTmpdirInstance } from "../fixture/fixture"
  8. import { testEffect } from "../lib/effect"
  9. const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
  10. describe("LSP service lifecycle", () => {
  11. let spawnSpy: ReturnType<typeof spyOn>
  12. beforeEach(() => {
  13. spawnSpy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
  14. })
  15. afterEach(() => {
  16. spawnSpy.mockRestore()
  17. })
  18. it.live("init() completes without error", () => provideTmpdirInstance(() => LSP.Service.use((lsp) => lsp.init())))
  19. it.live("status() returns empty array initially", () =>
  20. provideTmpdirInstance(() =>
  21. LSP.Service.use((lsp) =>
  22. Effect.gen(function* () {
  23. const result = yield* lsp.status()
  24. expect(Array.isArray(result)).toBe(true)
  25. expect(result.length).toBe(0)
  26. }),
  27. ),
  28. ),
  29. )
  30. it.live("diagnostics() returns empty object initially", () =>
  31. provideTmpdirInstance(() =>
  32. LSP.Service.use((lsp) =>
  33. Effect.gen(function* () {
  34. const result = yield* lsp.diagnostics()
  35. expect(typeof result).toBe("object")
  36. expect(Object.keys(result).length).toBe(0)
  37. }),
  38. ),
  39. ),
  40. )
  41. it.live("hasClients() returns false for .ts files in instance when LSP is unset", () =>
  42. provideTmpdirInstance((dir) =>
  43. LSP.Service.use((lsp) =>
  44. Effect.gen(function* () {
  45. const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
  46. expect(result).toBe(false)
  47. }),
  48. ),
  49. ),
  50. )
  51. it.live("hasClients() returns true for .ts files in instance when lsp is true", () =>
  52. provideTmpdirInstance(
  53. (dir) =>
  54. LSP.Service.use((lsp) =>
  55. Effect.gen(function* () {
  56. const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
  57. expect(result).toBe(true)
  58. }),
  59. ),
  60. { config: { lsp: true } },
  61. ),
  62. )
  63. it.live("hasClients() keeps built-in LSPs when config object is provided", () =>
  64. provideTmpdirInstance(
  65. (dir) =>
  66. LSP.Service.use((lsp) =>
  67. Effect.gen(function* () {
  68. const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
  69. expect(result).toBe(true)
  70. }),
  71. ),
  72. {
  73. config: {
  74. lsp: {
  75. eslint: { disabled: true },
  76. },
  77. },
  78. },
  79. ),
  80. )
  81. it.live("hasClients() returns false for files outside instance", () =>
  82. provideTmpdirInstance((dir) =>
  83. LSP.Service.use((lsp) =>
  84. Effect.gen(function* () {
  85. const result = yield* lsp.hasClients(path.join(dir, "..", "outside.ts"))
  86. expect(typeof result).toBe("boolean")
  87. }),
  88. ),
  89. ),
  90. )
  91. it.live("workspaceSymbol() returns empty array with no clients", () =>
  92. provideTmpdirInstance(() =>
  93. LSP.Service.use((lsp) =>
  94. Effect.gen(function* () {
  95. const result = yield* lsp.workspaceSymbol("test")
  96. expect(Array.isArray(result)).toBe(true)
  97. expect(result.length).toBe(0)
  98. }),
  99. ),
  100. ),
  101. )
  102. it.live("definition() returns empty array for unknown file", () =>
  103. provideTmpdirInstance((dir) =>
  104. LSP.Service.use((lsp) =>
  105. Effect.gen(function* () {
  106. const result = yield* lsp.definition({
  107. file: path.join(dir, "nonexistent.ts"),
  108. line: 0,
  109. character: 0,
  110. })
  111. expect(Array.isArray(result)).toBe(true)
  112. }),
  113. ),
  114. ),
  115. )
  116. it.live("references() returns empty array for unknown file", () =>
  117. provideTmpdirInstance((dir) =>
  118. LSP.Service.use((lsp) =>
  119. Effect.gen(function* () {
  120. const result = yield* lsp.references({
  121. file: path.join(dir, "nonexistent.ts"),
  122. line: 0,
  123. character: 0,
  124. })
  125. expect(Array.isArray(result)).toBe(true)
  126. }),
  127. ),
  128. ),
  129. )
  130. it.live("multiple init() calls are idempotent", () =>
  131. provideTmpdirInstance(() =>
  132. LSP.Service.use((lsp) =>
  133. Effect.gen(function* () {
  134. yield* lsp.init()
  135. yield* lsp.init()
  136. yield* lsp.init()
  137. }),
  138. ),
  139. ),
  140. )
  141. })
  142. describe("LSP.Diagnostic", () => {
  143. test("pretty() formats error diagnostic", () => {
  144. const result = LSP.Diagnostic.pretty({
  145. range: { start: { line: 9, character: 4 }, end: { line: 9, character: 10 } },
  146. message: "Type 'string' is not assignable to type 'number'",
  147. severity: 1,
  148. } as any)
  149. expect(result).toBe("ERROR [10:5] Type 'string' is not assignable to type 'number'")
  150. })
  151. test("pretty() formats warning diagnostic", () => {
  152. const result = LSP.Diagnostic.pretty({
  153. range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
  154. message: "Unused variable",
  155. severity: 2,
  156. } as any)
  157. expect(result).toBe("WARN [1:1] Unused variable")
  158. })
  159. test("pretty() defaults to ERROR when no severity", () => {
  160. const result = LSP.Diagnostic.pretty({
  161. range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
  162. message: "Something wrong",
  163. } as any)
  164. expect(result).toBe("ERROR [1:1] Something wrong")
  165. })
  166. })