oh-my-opencode 5.0.0-beta.31 → 5.0.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/dist/cli/index.js +136 -44
  2. package/dist/cli-node/index.js +136 -44
  3. package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
  4. package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
  5. package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
  6. package/dist/features/monitor/manager-internals.d.ts +1 -1
  7. package/dist/features/monitor/output-injector-types.d.ts +1 -1
  8. package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
  9. package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
  10. package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
  11. package/dist/index.js +286 -99
  12. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  13. package/dist/tools/session-manager/directory-filter.d.ts +2 -0
  14. package/dist/tui.js +109 -23
  15. package/package.json +16 -15
  16. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
  17. package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
  18. package/packages/lsp-core/src/lsp/client.ts +6 -3
  19. package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
  20. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
  21. package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
  22. package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
  23. package/packages/lsp-core/src/lsp/transport.ts +7 -2
  24. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
  25. package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
  26. package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
  27. package/packages/lsp-daemon/dist/cli.js +41 -16
  28. package/packages/lsp-daemon/dist/client.js +41 -16
  29. package/packages/lsp-daemon/dist/index.js +41 -16
  30. package/packages/lsp-tools-mcp/dist/cli.js +41 -16
  31. package/packages/lsp-tools-mcp/dist/lsp/manager.js +41 -16
  32. package/packages/lsp-tools-mcp/dist/mcp.js +41 -16
  33. package/packages/lsp-tools-mcp/dist/tools.js +41 -16
  34. package/packages/lsp-tools-mcp/package.json +1 -1
  35. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  36. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  37. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +3 -17
  39. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +3 -17
  40. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  44. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  46. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  48. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +39 -16
  49. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  52. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  53. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
  58. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
  59. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  60. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
  61. package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
  62. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
  63. package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
  64. package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
  65. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
  66. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  67. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  68. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +4 -2
  69. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -5
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -0
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +775 -312
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +18 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -99
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
  95. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +184 -14
  96. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
  97. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +3 -0
  99. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +15 -0
  100. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  101. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  102. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +17 -0
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +8 -12
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +18 -0
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
  118. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +50 -116
  119. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
  120. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +186 -12
  121. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +22 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +66 -3
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +13 -3
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +16 -15
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +300 -3
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
  134. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  135. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  136. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  137. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  138. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  139. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  140. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  141. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  157. package/packages/omo-codex/plugin/package-lock.json +13 -13
  158. package/packages/omo-codex/plugin/package.json +1 -1
  159. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
  160. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  161. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
  162. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
  163. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
  164. package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
  165. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  166. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  167. package/dist/features/builtin-skills/index.d.ts +0 -1
  168. package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
  169. package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
  170. package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
  171. package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
  172. package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
  173. package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
  174. package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
  175. package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
  176. package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
  177. package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
  178. package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
  179. package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
  180. package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
  181. package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
  182. package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
  183. package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
  184. package/dist/features/builtin-skills/skills/index.d.ts +0 -1
  185. package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
  186. package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
  187. package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
  188. package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
  189. package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
  190. package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
  191. package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
  192. package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
  193. package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
  194. package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
  195. package/dist/features/builtin-skills/skills.d.ts +0 -1
  196. package/dist/features/builtin-skills/types.d.ts +0 -1
  197. package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
  198. package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
  199. package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
  200. package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
  201. package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
  202. package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
  203. package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
  204. package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
  205. package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
  206. package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
  207. package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
  208. package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
  209. package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
  210. package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
  211. package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
  212. package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
  213. package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
  214. package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
  215. package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
  216. package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
  217. package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
  218. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
  219. package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
  220. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
  221. package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
  222. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
  223. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
  224. package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
  225. package/dist/shared/prompt-async-gate/types.d.ts +0 -1
  226. package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
  227. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
@@ -0,0 +1,73 @@
1
+ import { PassThrough } from "node:stream";
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+
5
+ import { ControlledClock } from "./controlled-clock-test-support.js";
6
+ import { LspRequestTimeoutError } from "./errors.js";
7
+ import { JsonRpcConnection } from "./json-rpc-connection.js";
8
+ import { LspClientTransport } from "./transport.js";
9
+ import type { LspClientTimeoutOptions } from "./transport.js";
10
+ import type { ResolvedServer } from "./types.js";
11
+
12
+ const fakeServer: ResolvedServer = {
13
+ id: "controlled-clock-fake",
14
+ command: ["controlled-clock-fake"],
15
+ extensions: [".ts"],
16
+ priority: 0,
17
+ };
18
+
19
+ class WiredTransport extends LspClientTransport {
20
+ constructor(timeouts: LspClientTimeoutOptions, connection: JsonRpcConnection) {
21
+ super("/tmp", fakeServer, timeouts);
22
+ this.connection = connection;
23
+ }
24
+
25
+ request<T>(method: string, params: unknown, options: { timeoutMs?: number }): Promise<T> {
26
+ return this.sendRequest<T>(method, params, options);
27
+ }
28
+ }
29
+
30
+ function encodeMessage(message: Record<string, unknown>): string {
31
+ const body = JSON.stringify(message);
32
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
33
+ }
34
+
35
+ function makeWiredTransport(clock: ControlledClock): { transport: WiredTransport; serverToClient: PassThrough } {
36
+ const serverToClient = new PassThrough();
37
+ const clientToServer = new PassThrough();
38
+ const connection = new JsonRpcConnection(serverToClient, clientToServer);
39
+ connection.listen();
40
+ return { transport: new WiredTransport({ timerProvider: clock }, connection), serverToClient };
41
+ }
42
+
43
+ describe("LspClientTransport request timeout via TimerProvider", () => {
44
+ it("#given a request the server never answers #when the injected clock passes the timeout #then the request rejects with LspRequestTimeoutError", async () => {
45
+ const clock = new ControlledClock();
46
+ const { transport } = makeWiredTransport(clock);
47
+
48
+ const pending = transport.request("textDocument/diagnostic", { textDocument: { uri: "file:///a.ts" } }, {
49
+ timeoutMs: 50,
50
+ });
51
+ await clock.waitForTimer(50);
52
+ expect(clock.scheduledDelays.at(-1)).toBe(50);
53
+ clock.advanceBy(50);
54
+
55
+ await expect(pending).rejects.toBeInstanceOf(LspRequestTimeoutError);
56
+ });
57
+
58
+ it("#given a request answered before the deadline #when the injected clock later passes the timeout #then the settled result stands", async () => {
59
+ const clock = new ControlledClock();
60
+ const { transport, serverToClient } = makeWiredTransport(clock);
61
+
62
+ const pending = transport.request<{ items: unknown[] }>(
63
+ "textDocument/diagnostic",
64
+ { textDocument: { uri: "file:///a.ts" } },
65
+ { timeoutMs: 50 },
66
+ );
67
+ await clock.waitForTimer(50);
68
+ serverToClient.write(encodeMessage({ jsonrpc: "2.0", id: 1, result: { items: [] } }));
69
+
70
+ await expect(pending).resolves.toEqual({ items: [] });
71
+ clock.advanceBy(50);
72
+ });
73
+ });
@@ -1,4 +1,6 @@
1
1
  import { reportBestEffortCleanupError } from "./cleanup-errors.js";
2
+ import { realTimerProvider } from "./timer-provider.js";
3
+ import type { TimerProvider } from "./timer-provider.js";
2
4
  import { INIT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, STOP_HARD_KILL_TIMEOUT_MS, STOP_SIGKILL_GRACE_MS } from "./constants.js";
3
5
  import { LspConnectionClosedError, LspProcessExitedError, LspRequestTimeoutError } from "./errors.js";
4
6
  import { JsonRpcConnection } from "./json-rpc-connection.js";
@@ -12,6 +14,7 @@ export { createLspSpawnEnv } from "./transport-protocol.js";
12
14
  export interface LspClientTimeoutOptions {
13
15
  requestTimeoutMs?: number;
14
16
  initializeTimeoutMs?: number;
17
+ timerProvider?: TimerProvider;
15
18
  }
16
19
 
17
20
  type WorkspaceApplyEditHandler = (params: unknown) => Promise<WorkspaceApplyEditResponse>;
@@ -35,6 +38,7 @@ export class LspClientTransport {
35
38
  protected readonly diagnosticsStore = new Map<string, Diagnostic[]>();
36
39
  protected readonly requestTimeoutMs: number;
37
40
  protected readonly initializeTimeoutMs: number;
41
+ protected readonly timerProvider: TimerProvider;
38
42
  private workspaceApplyEditHandler: WorkspaceApplyEditHandler | null = null;
39
43
  private diagnosticPullSupported = false;
40
44
  private documentFormattingSupported = false;
@@ -46,6 +50,7 @@ export class LspClientTransport {
46
50
  ) {
47
51
  this.requestTimeoutMs = timeouts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
48
52
  this.initializeTimeoutMs = timeouts.initializeTimeoutMs ?? INIT_TIMEOUT_MS;
53
+ this.timerProvider = timeouts.timerProvider ?? realTimerProvider;
49
54
  }
50
55
 
51
56
  pid(): number | undefined {
@@ -187,7 +192,7 @@ export class LspClientTransport {
187
192
  const options = args[1];
188
193
  const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
189
194
  const timeoutController = new AbortController();
190
- const timeoutHandle = setTimeout(() => {
195
+ const timeoutHandle = this.timerProvider.setTimeout(() => {
191
196
  const stderrTail = this.stderrBuffer.slice(-5).join("\n");
192
197
  timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
193
198
  }, timeoutMs);
@@ -213,7 +218,7 @@ export class LspClientTransport {
213
218
  }
214
219
  throw error;
215
220
  } finally {
216
- clearTimeout(timeoutHandle);
221
+ this.timerProvider.clearTimeout(timeoutHandle);
217
222
  combinedSignal.dispose();
218
223
  }
219
224
  }
@@ -1,8 +1,91 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
2
5
 
6
+ import { afterEach, describe, expect, it } from "bun:test";
7
+
8
+ import type { Diagnostic } from "./types.js";
3
9
  import { WorkspaceDocumentState } from "./workspace-document-state.js";
4
10
  import type { WorkspaceMutation } from "./workspace-edit.js";
5
11
 
12
+ const workspaces: string[] = [];
13
+
14
+ afterEach(() => {
15
+ while (workspaces.length > 0) {
16
+ const workspace = workspaces.pop();
17
+ if (workspace) rmSync(workspace, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ function makeDocuments(): WorkspaceDocumentState {
22
+ return new WorkspaceDocumentState(
23
+ async () => {},
24
+ () => {},
25
+ { versionlessPublishQuiescenceMs: 0 },
26
+ );
27
+ }
28
+
29
+ function makeSourceFile(directorySegment: string): string {
30
+ const workspace = realpathSync(mkdtempSync(join(tmpdir(), "lsp-document-uri-")));
31
+ workspaces.push(workspace);
32
+ const directory = join(workspace, directorySegment);
33
+ mkdirSync(directory, { recursive: true });
34
+ const source = join(directory, "page.ts");
35
+ writeFileSync(source, "const value: number = 'text';\n", "utf-8");
36
+ return source;
37
+ }
38
+
39
+ const diagnostic: Diagnostic = {
40
+ range: { start: { line: 0, character: 6 }, end: { line: 0, character: 11 } },
41
+ severity: 1,
42
+ message: "Type 'string' is not assignable to type 'number'.",
43
+ };
44
+
45
+ describe("WorkspaceDocumentState published diagnostics under a re-encoded uri", () => {
46
+ it("#given a server that percent-encodes a path segment #when it publishes #then the diagnostics reach the open document", async () => {
47
+ const documents = makeDocuments();
48
+ const source = makeSourceFile("(marketing)");
49
+ await documents.openFile(source);
50
+ const snapshot = documents.captureDiagnosticSnapshot(source);
51
+ expect(snapshot).not.toBeNull();
52
+ const publishedUri = snapshot!.uri.replaceAll("(", "%28").replaceAll(")", "%29");
53
+ expect(publishedUri).not.toBe(snapshot!.uri);
54
+
55
+ documents.recordPublishedDiagnostics({ uri: publishedUri, diagnostics: [diagnostic], version: snapshot!.version });
56
+
57
+ expect(documents.getStoredDiagnostics(snapshot!.uri)).toEqual([diagnostic]);
58
+ expect(documents.resolvePushDiagnostics(snapshot!)).toEqual({ status: "ready", diagnostics: [diagnostic] });
59
+ });
60
+
61
+ it.skipIf(process.platform !== "win32")(
62
+ "#given a server that lowercases and percent-encodes the drive #when it publishes #then the diagnostics reach the open document",
63
+ async () => {
64
+ const documents = makeDocuments();
65
+ const source = makeSourceFile("routes");
66
+ await documents.openFile(source);
67
+ const snapshot = documents.captureDiagnosticSnapshot(source);
68
+ expect(snapshot).not.toBeNull();
69
+ const publishedUri = snapshot!.uri.replace(/^file:\/\/\/([A-Za-z]):/, (_match, drive: string) => `file:///${drive.toLowerCase()}%3A`);
70
+ expect(publishedUri).not.toBe(snapshot!.uri);
71
+
72
+ documents.recordPublishedDiagnostics({ uri: publishedUri, diagnostics: [diagnostic], version: snapshot!.version });
73
+
74
+ expect(documents.getStoredDiagnostics(snapshot!.uri)).toEqual([diagnostic]);
75
+ expect(documents.resolvePushDiagnostics(snapshot!)).toEqual({ status: "ready", diagnostics: [diagnostic] });
76
+ },
77
+ );
78
+
79
+ it("#given a document opened from a path the client already encodes #when the server echoes it verbatim #then the outgoing uri is unchanged", async () => {
80
+ const documents = makeDocuments();
81
+ const source = makeSourceFile("plain");
82
+ await documents.openFile(source);
83
+ const snapshot = documents.captureDiagnosticSnapshot(source);
84
+
85
+ expect(snapshot?.uri).toBe(pathToFileURL(source).href);
86
+ });
87
+ });
88
+
6
89
  describe("WorkspaceDocumentState watched-file bounds", () => {
7
90
  it("#given more closed-file mutations than one batch #when synchronized #then every notification stays bounded", async () => {
8
91
  const notifications: unknown[] = [];
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
5
5
  import { effectiveExtension } from "./effective-extension.js";
6
6
  import { getLanguageId } from "./language-mappings.js";
7
7
  import type { Diagnostic } from "./types.js";
8
+ import type { TimerProvider } from "./timer-provider.js";
8
9
  import type { PlannedWorkspaceOperation, WorkspaceMutation, WorkspaceMutationDelta } from "./workspace-edit-types.js";
9
10
 
10
11
  const WATCHED_FILE_BATCH_SIZE = 128;
@@ -76,6 +77,7 @@ export type PushDiagnosticsResolution =
76
77
 
77
78
  export interface WorkspaceDocumentStateOptions {
78
79
  readonly now?: () => number;
80
+ readonly timerProvider?: TimerProvider;
79
81
  readonly versionlessPublishQuiescenceMs?: number;
80
82
  }
81
83
 
@@ -88,6 +90,17 @@ function canonicalPath(filePath: string): string {
88
90
  }
89
91
  }
90
92
 
93
+ function normalizeDocumentUri(uri: string): string {
94
+ let decoded = uri;
95
+ try {
96
+ decoded = decodeURIComponent(uri);
97
+ } catch {
98
+ decoded = uri;
99
+ }
100
+ if (process.platform !== "win32") return decoded;
101
+ return decoded.replace(/^(file:\/\/\/)([a-z]):/, (_match, prefix: string, drive: string) => `${prefix}${drive.toUpperCase()}:`);
102
+ }
103
+
91
104
  function isSameOrDescendant(candidate: string, parent: string): boolean {
92
105
  const suffix = relative(parent, candidate);
93
106
  return suffix === "" || (!suffix.startsWith("..") && suffix !== "..");
@@ -103,6 +116,7 @@ export class WorkspaceDocumentState {
103
116
  private readonly openByUri = new Map<string, OpenDocumentState>();
104
117
  private readonly openPromises = new Map<string, Promise<void>>();
105
118
  private readonly now: () => number;
119
+ private readonly timerProvider: TimerProvider;
106
120
  private readonly versionlessPublishQuiescenceMs: number;
107
121
 
108
122
  constructor(
@@ -110,7 +124,12 @@ export class WorkspaceDocumentState {
110
124
  private readonly clearDiagnostics: (uri: string) => void,
111
125
  options: WorkspaceDocumentStateOptions = {},
112
126
  ) {
113
- this.now = options.now ?? (() => Date.now());
127
+ this.timerProvider = options.timerProvider ?? {
128
+ now: options.now ?? (() => Date.now()),
129
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
130
+ clearTimeout: (handle) => clearTimeout(handle),
131
+ };
132
+ this.now = () => this.timerProvider.now();
114
133
  this.versionlessPublishQuiescenceMs =
115
134
  options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
116
135
  }
@@ -135,7 +154,7 @@ export class WorkspaceDocumentState {
135
154
  }
136
155
 
137
156
  getStoredDiagnostics(uri: string): readonly Diagnostic[] {
138
- const state = this.openByUri.get(uri);
157
+ const state = this.openByUri.get(normalizeDocumentUri(uri));
139
158
  if (!state) return [];
140
159
  return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
141
160
  }
@@ -163,13 +182,13 @@ export class WorkspaceDocumentState {
163
182
  }
164
183
 
165
184
  getPullCache(snapshot: DiagnosticSnapshot): PullDiagnosticsCacheHit | null {
166
- const state = this.openByUri.get(snapshot.uri);
185
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
167
186
  if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version) return null;
168
187
  return state.pullCache;
169
188
  }
170
189
 
171
190
  recordPullDiagnostics(snapshot: DiagnosticSnapshot, report: PullDiagnosticsReport): void {
172
- const state = this.openByUri.get(snapshot.uri);
191
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
173
192
  if (!state) return;
174
193
  state.pullCache = {
175
194
  documentVersion: snapshot.version,
@@ -183,7 +202,7 @@ export class WorkspaceDocumentState {
183
202
  readonly diagnostics: readonly Diagnostic[];
184
203
  readonly version?: number;
185
204
  }): void {
186
- const state = this.openByUri.get(params.uri);
205
+ const state = this.openByUri.get(normalizeDocumentUri(params.uri));
187
206
  if (!state) return;
188
207
  state.publishGeneration += 1;
189
208
  state.lastPublish = {
@@ -197,7 +216,7 @@ export class WorkspaceDocumentState {
197
216
  }
198
217
 
199
218
  resolvePushDiagnostics(snapshot: DiagnosticSnapshot): PushDiagnosticsResolution {
200
- const state = this.openByUri.get(snapshot.uri);
219
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
201
220
  if (!state?.lastPublish) return { status: "missing" };
202
221
  const publish = state.lastPublish;
203
222
  if (publish.version !== undefined) {
@@ -214,18 +233,18 @@ export class WorkspaceDocumentState {
214
233
  }
215
234
 
216
235
  waitForDiagnosticsActivity(snapshot: DiagnosticSnapshot, timeoutMs: number): Promise<void> {
217
- const state = this.openByUri.get(snapshot.uri);
236
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
218
237
  if (!state || timeoutMs <= 0) return Promise.resolve();
219
238
  return new Promise((resolveActivity) => {
220
239
  let settled = false;
221
240
  const finish = () => {
222
241
  if (settled) return;
223
242
  settled = true;
224
- clearTimeout(timer);
243
+ this.timerProvider.clearTimeout(timer);
225
244
  state.waiters.delete(finish);
226
245
  resolveActivity();
227
246
  };
228
- const timer = setTimeout(finish, timeoutMs);
247
+ const timer = this.timerProvider.setTimeout(finish, timeoutMs);
229
248
  if (typeof timer.unref === "function") timer.unref();
230
249
  state.waiters.add(finish);
231
250
  });
@@ -329,7 +348,7 @@ export class WorkspaceDocumentState {
329
348
  waiters: new Set(),
330
349
  };
331
350
  this.openDocuments.set(path, state);
332
- this.openByUri.set(state.uri, state);
351
+ this.openByUri.set(normalizeDocumentUri(state.uri), state);
333
352
  this.notifyWaiters(state);
334
353
  await this.sendNotification("textDocument/didOpen", {
335
354
  textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text },
@@ -356,7 +375,7 @@ export class WorkspaceDocumentState {
356
375
 
357
376
  private async closeDocument(state: OpenDocumentState): Promise<void> {
358
377
  this.openDocuments.delete(state.path);
359
- this.openByUri.delete(state.uri);
378
+ this.openByUri.delete(normalizeDocumentUri(state.uri));
360
379
  this.clearDiagnostics(state.uri);
361
380
  this.notifyWaiters(state);
362
381
  await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ clientOptions,
5
+ optionalBoolean,
6
+ optionalNumber,
7
+ optionalString,
8
+ requireNumber,
9
+ requireString,
10
+ severityFilter,
11
+ } from "./parameters.js";
12
+
13
+ describe("LSP tool parameter helpers", () => {
14
+ test("#given required string parameter #when value is missing or empty #then throws clear error", () => {
15
+ expect(requireString({ filePath: "index.ts" }, "filePath")).toBe("index.ts");
16
+ expect(() => requireString({}, "filePath")).toThrow("Missing required string parameter 'filePath'");
17
+ expect(() => requireString({ filePath: "" }, "filePath")).toThrow("Missing required string parameter 'filePath'");
18
+ expect(() => requireString({ filePath: 42 }, "filePath")).toThrow("Missing required string parameter 'filePath'");
19
+ });
20
+
21
+ test("#given required number parameter #when value is non-finite #then throws clear error", () => {
22
+ expect(requireNumber({ line: 3 }, "line")).toBe(3);
23
+ expect(() => requireNumber({}, "line")).toThrow("Missing required number parameter 'line'");
24
+ expect(() => requireNumber({ line: Number.NaN }, "line")).toThrow("Missing required number parameter 'line'");
25
+ expect(() => requireNumber({ line: Number.POSITIVE_INFINITY }, "line")).toThrow(
26
+ "Missing required number parameter 'line'",
27
+ );
28
+ });
29
+
30
+ test("#given optional parameters #when values have wrong type #then ignores them", () => {
31
+ expect(optionalString({ query: "symbol" }, "query")).toBe("symbol");
32
+ expect(optionalString({ query: 7 }, "query")).toBeUndefined();
33
+ expect(optionalNumber({ limit: 10 }, "limit")).toBe(10);
34
+ expect(optionalNumber({ limit: Number.NaN }, "limit")).toBeUndefined();
35
+ expect(optionalBoolean({ includeDeclaration: false }, "includeDeclaration")).toBe(false);
36
+ expect(optionalBoolean({ includeDeclaration: "false" }, "includeDeclaration")).toBeUndefined();
37
+ });
38
+
39
+ test("#given severity parameter #when value is unknown #then defaults to all", () => {
40
+ expect(severityFilter({ severity: "warning" })).toBe("warning");
41
+ expect(severityFilter({ severity: "fatal" })).toBe("all");
42
+ expect(severityFilter({})).toBe("all");
43
+ });
44
+
45
+ test("#given optional abort signal #when building client options #then includes only defined signal", () => {
46
+ const controller = new AbortController();
47
+
48
+ expect(clientOptions(undefined)).toEqual({});
49
+ expect(clientOptions(controller.signal)).toEqual({ signal: controller.signal });
50
+ });
51
+ });
@@ -595,6 +595,13 @@ import { pathToFileURL as pathToFileURL3 } from "node:url";
595
595
  // ../lsp-core/src/lsp/connection.ts
596
596
  import { pathToFileURL } from "node:url";
597
597
 
598
+ // ../lsp-core/src/lsp/timer-provider.ts
599
+ var realTimerProvider = {
600
+ now: () => Date.now(),
601
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
602
+ clearTimeout: (handle) => clearTimeout(handle)
603
+ };
604
+
598
605
  // ../lsp-core/src/lsp/constants.ts
599
606
  var DEFAULT_MAX_REFERENCES = 200;
600
607
  var DEFAULT_MAX_SYMBOLS = 200;
@@ -1134,6 +1141,7 @@ class LspClientTransport {
1134
1141
  this.documentFormattingSupported = false;
1135
1142
  this.requestTimeoutMs = timeouts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
1136
1143
  this.initializeTimeoutMs = timeouts.initializeTimeoutMs ?? INIT_TIMEOUT_MS;
1144
+ this.timerProvider = timeouts.timerProvider ?? realTimerProvider;
1137
1145
  }
1138
1146
  pid() {
1139
1147
  return this.proc?.pid;
@@ -1234,7 +1242,7 @@ class LspClientTransport {
1234
1242
  const options = args[1];
1235
1243
  const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
1236
1244
  const timeoutController = new AbortController;
1237
- const timeoutHandle = setTimeout(() => {
1245
+ const timeoutHandle = this.timerProvider.setTimeout(() => {
1238
1246
  const stderrTail = this.stderrBuffer.slice(-5).join(`
1239
1247
  `);
1240
1248
  timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
@@ -1253,7 +1261,7 @@ class LspClientTransport {
1253
1261
  }
1254
1262
  throw error;
1255
1263
  } finally {
1256
- clearTimeout(timeoutHandle);
1264
+ this.timerProvider.clearTimeout(timeoutHandle);
1257
1265
  combinedSignal.dispose();
1258
1266
  }
1259
1267
  }
@@ -1634,6 +1642,17 @@ function canonicalPath(filePath) {
1634
1642
  return absolute;
1635
1643
  }
1636
1644
  }
1645
+ function normalizeDocumentUri(uri) {
1646
+ let decoded = uri;
1647
+ try {
1648
+ decoded = decodeURIComponent(uri);
1649
+ } catch {
1650
+ decoded = uri;
1651
+ }
1652
+ if (process.platform !== "win32")
1653
+ return decoded;
1654
+ return decoded.replace(/^(file:\/\/\/)([a-z]):/, (_match, prefix, drive) => `${prefix}${drive.toUpperCase()}:`);
1655
+ }
1637
1656
  function isSameOrDescendant(candidate, parent) {
1638
1657
  const suffix = relative2(parent, candidate);
1639
1658
  return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
@@ -1650,7 +1669,12 @@ class WorkspaceDocumentState {
1650
1669
  this.openDocuments = new Map;
1651
1670
  this.openByUri = new Map;
1652
1671
  this.openPromises = new Map;
1653
- this.now = options.now ?? (() => Date.now());
1672
+ this.timerProvider = options.timerProvider ?? {
1673
+ now: options.now ?? (() => Date.now()),
1674
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
1675
+ clearTimeout: (handle) => clearTimeout(handle)
1676
+ };
1677
+ this.now = () => this.timerProvider.now();
1654
1678
  this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
1655
1679
  }
1656
1680
  async openFile(filePath) {
@@ -1672,7 +1696,7 @@ class WorkspaceDocumentState {
1672
1696
  return this.openDocuments.get(canonicalPath(filePath))?.version;
1673
1697
  }
1674
1698
  getStoredDiagnostics(uri) {
1675
- const state = this.openByUri.get(uri);
1699
+ const state = this.openByUri.get(normalizeDocumentUri(uri));
1676
1700
  if (!state)
1677
1701
  return [];
1678
1702
  return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
@@ -1694,13 +1718,13 @@ class WorkspaceDocumentState {
1694
1718
  return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
1695
1719
  }
1696
1720
  getPullCache(snapshot) {
1697
- const state = this.openByUri.get(snapshot.uri);
1721
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
1698
1722
  if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
1699
1723
  return null;
1700
1724
  return state.pullCache;
1701
1725
  }
1702
1726
  recordPullDiagnostics(snapshot, report) {
1703
- const state = this.openByUri.get(snapshot.uri);
1727
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
1704
1728
  if (!state)
1705
1729
  return;
1706
1730
  state.pullCache = {
@@ -1710,7 +1734,7 @@ class WorkspaceDocumentState {
1710
1734
  };
1711
1735
  }
1712
1736
  recordPublishedDiagnostics(params) {
1713
- const state = this.openByUri.get(params.uri);
1737
+ const state = this.openByUri.get(normalizeDocumentUri(params.uri));
1714
1738
  if (!state)
1715
1739
  return;
1716
1740
  state.publishGeneration += 1;
@@ -1724,7 +1748,7 @@ class WorkspaceDocumentState {
1724
1748
  this.notifyWaiters(state);
1725
1749
  }
1726
1750
  resolvePushDiagnostics(snapshot) {
1727
- const state = this.openByUri.get(snapshot.uri);
1751
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
1728
1752
  if (!state?.lastPublish)
1729
1753
  return { status: "missing" };
1730
1754
  const publish = state.lastPublish;
@@ -1738,7 +1762,7 @@ class WorkspaceDocumentState {
1738
1762
  return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
1739
1763
  }
1740
1764
  waitForDiagnosticsActivity(snapshot, timeoutMs) {
1741
- const state = this.openByUri.get(snapshot.uri);
1765
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
1742
1766
  if (!state || timeoutMs <= 0)
1743
1767
  return Promise.resolve();
1744
1768
  return new Promise((resolveActivity) => {
@@ -1747,11 +1771,11 @@ class WorkspaceDocumentState {
1747
1771
  if (settled)
1748
1772
  return;
1749
1773
  settled = true;
1750
- clearTimeout(timer);
1774
+ this.timerProvider.clearTimeout(timer);
1751
1775
  state.waiters.delete(finish);
1752
1776
  resolveActivity();
1753
1777
  };
1754
- const timer = setTimeout(finish, timeoutMs);
1778
+ const timer = this.timerProvider.setTimeout(finish, timeoutMs);
1755
1779
  if (typeof timer.unref === "function")
1756
1780
  timer.unref();
1757
1781
  state.waiters.add(finish);
@@ -1859,7 +1883,7 @@ class WorkspaceDocumentState {
1859
1883
  waiters: new Set
1860
1884
  };
1861
1885
  this.openDocuments.set(path, state);
1862
- this.openByUri.set(state.uri, state);
1886
+ this.openByUri.set(normalizeDocumentUri(state.uri), state);
1863
1887
  this.notifyWaiters(state);
1864
1888
  await this.sendNotification("textDocument/didOpen", {
1865
1889
  textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
@@ -1884,7 +1908,7 @@ class WorkspaceDocumentState {
1884
1908
  }
1885
1909
  async closeDocument(state) {
1886
1910
  this.openDocuments.delete(state.path);
1887
- this.openByUri.delete(state.uri);
1911
+ this.openByUri.delete(normalizeDocumentUri(state.uri));
1888
1912
  this.clearDiagnostics(state.uri);
1889
1913
  this.notifyWaiters(state);
1890
1914
  await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
@@ -2971,6 +2995,7 @@ class LspClient extends LspClientConnection {
2971
2995
  this.diagnosticPullErrors = [];
2972
2996
  this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
2973
2997
  this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
2998
+ timerProvider: this.timerProvider,
2974
2999
  versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
2975
3000
  });
2976
3001
  this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
@@ -3074,7 +3099,7 @@ class LspClient extends LspClientConnection {
3074
3099
  const absPath = this.resolveWorkspacePath(filePath);
3075
3100
  const uri = pathToFileURL3(absPath).href;
3076
3101
  await this.openFile(absPath);
3077
- const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
3102
+ const deadlineAt = this.timerProvider.now() + this.diagnosticsFreshnessTimeoutMs;
3078
3103
  for (;; ) {
3079
3104
  signal?.throwIfAborted();
3080
3105
  const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
@@ -3087,7 +3112,7 @@ class LspClient extends LspClientConnection {
3087
3112
  if (!pushFallbackOnly) {
3088
3113
  const cached = this.documents.getPullCache(snapshot);
3089
3114
  try {
3090
- const remainingMs2 = deadlineAt - Date.now();
3115
+ const remainingMs2 = deadlineAt - this.timerProvider.now();
3091
3116
  if (remainingMs2 <= 0)
3092
3117
  return this.freshnessTimeout(absPath);
3093
3118
  const result = await this.sendRequest("textDocument/diagnostic", {
@@ -3122,7 +3147,7 @@ class LspClient extends LspClientConnection {
3122
3147
  }
3123
3148
  if (!pushFallbackOnly)
3124
3149
  continue;
3125
- const remainingMs = deadlineAt - Date.now();
3150
+ const remainingMs = deadlineAt - this.timerProvider.now();
3126
3151
  if (remainingMs <= 0) {
3127
3152
  if (!this.isDiagnosticPullSupported() && snapshot.publishGeneration === 0) {
3128
3153
  const cached = this.documents.getPullCache(snapshot);