lifecycle.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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/server"
  6. import * as CrossSpawnSpawner from "../../src/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 true for .ts files in instance", () =>
  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(true)
  47. }),
  48. ),
  49. ),
  50. )
  51. it.live("hasClients() returns false for files outside instance", () =>
  52. provideTmpdirInstance((dir) =>
  53. LSP.Service.use((lsp) =>
  54. Effect.gen(function* () {
  55. const result = yield* lsp.hasClients(path.join(dir, "..", "outside.ts"))
  56. expect(typeof result).toBe("boolean")
  57. }),
  58. ),
  59. ),
  60. )
  61. it.live("workspaceSymbol() returns empty array with no clients", () =>
  62. provideTmpdirInstance(() =>
  63. LSP.Service.use((lsp) =>
  64. Effect.gen(function* () {
  65. const result = yield* lsp.workspaceSymbol("test")
  66. expect(Array.isArray(result)).toBe(true)
  67. expect(result.length).toBe(0)
  68. }),
  69. ),
  70. ),
  71. )
  72. it.live("definition() returns empty array for unknown file", () =>
  73. provideTmpdirInstance((dir) =>
  74. LSP.Service.use((lsp) =>
  75. Effect.gen(function* () {
  76. const result = yield* lsp.definition({
  77. file: path.join(dir, "nonexistent.ts"),
  78. line: 0,
  79. character: 0,
  80. })
  81. expect(Array.isArray(result)).toBe(true)
  82. }),
  83. ),
  84. ),
  85. )
  86. it.live("references() returns empty array for unknown file", () =>
  87. provideTmpdirInstance((dir) =>
  88. LSP.Service.use((lsp) =>
  89. Effect.gen(function* () {
  90. const result = yield* lsp.references({
  91. file: path.join(dir, "nonexistent.ts"),
  92. line: 0,
  93. character: 0,
  94. })
  95. expect(Array.isArray(result)).toBe(true)
  96. }),
  97. ),
  98. ),
  99. )
  100. it.live("multiple init() calls are idempotent", () =>
  101. provideTmpdirInstance(() =>
  102. LSP.Service.use((lsp) =>
  103. Effect.gen(function* () {
  104. yield* lsp.init()
  105. yield* lsp.init()
  106. yield* lsp.init()
  107. }),
  108. ),
  109. ),
  110. )
  111. })
  112. describe("LSP.Diagnostic", () => {
  113. test("pretty() formats error diagnostic", () => {
  114. const result = LSP.Diagnostic.pretty({
  115. range: { start: { line: 9, character: 4 }, end: { line: 9, character: 10 } },
  116. message: "Type 'string' is not assignable to type 'number'",
  117. severity: 1,
  118. } as any)
  119. expect(result).toBe("ERROR [10:5] Type 'string' is not assignable to type 'number'")
  120. })
  121. test("pretty() formats warning diagnostic", () => {
  122. const result = LSP.Diagnostic.pretty({
  123. range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
  124. message: "Unused variable",
  125. severity: 2,
  126. } as any)
  127. expect(result).toBe("WARN [1:1] Unused variable")
  128. })
  129. test("pretty() defaults to ERROR when no severity", () => {
  130. const result = LSP.Diagnostic.pretty({
  131. range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
  132. message: "Something wrong",
  133. } as any)
  134. expect(result).toBe("ERROR [1:1] Something wrong")
  135. })
  136. })