@pi-in-go/pigpen-ahp 0.1.0

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 (304) hide show
  1. package/CREDITS.md +27 -0
  2. package/LICENSE +23 -0
  3. package/README.md +148 -0
  4. package/extensions/ahp/access_options_test.go +116 -0
  5. package/extensions/ahp/extension.go +54 -0
  6. package/extensions/ahp/go.mod +12 -0
  7. package/extensions/ahp/go.sum +2 -0
  8. package/extensions/ahp/go.work +8 -0
  9. package/extensions/ahp/internal/channels/chat.go +110 -0
  10. package/extensions/ahp/internal/channels/root.go +32 -0
  11. package/extensions/ahp/internal/channels/session.go +128 -0
  12. package/extensions/ahp/internal/channels/session_test.go +108 -0
  13. package/extensions/ahp/internal/compose/compose.go +153 -0
  14. package/extensions/ahp/internal/compose/compose_test.go +120 -0
  15. package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
  16. package/extensions/ahp/internal/host/handshake_test.go +206 -0
  17. package/extensions/ahp/internal/host/host.go +1305 -0
  18. package/extensions/ahp/internal/host/reconnect_test.go +197 -0
  19. package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
  20. package/extensions/ahp/internal/host/schema_test.go +79 -0
  21. package/extensions/ahp/internal/host/store.go +291 -0
  22. package/extensions/ahp/internal/host/store_update_test.go +85 -0
  23. package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
  24. package/extensions/ahp/internal/host/surface_test.go +95 -0
  25. package/extensions/ahp/internal/host/workarounds.go +458 -0
  26. package/extensions/ahp/internal/host/workarounds_test.go +96 -0
  27. package/extensions/ahp/internal/live/live.go +328 -0
  28. package/extensions/ahp/internal/live/live_test.go +91 -0
  29. package/extensions/ahp/internal/live/store.go +113 -0
  30. package/extensions/ahp/internal/mapper/activity.go +199 -0
  31. package/extensions/ahp/internal/mapper/activity_test.go +188 -0
  32. package/extensions/ahp/internal/mapper/bench_test.go +38 -0
  33. package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
  34. package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
  35. package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
  36. package/extensions/ahp/internal/mapper/js.go +131 -0
  37. package/extensions/ahp/internal/mapper/mapper.go +600 -0
  38. package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
  39. package/extensions/ahp/internal/mapper/messageinput.go +272 -0
  40. package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
  41. package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
  42. package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
  43. package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
  44. package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
  45. package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
  46. package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
  47. package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
  48. package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
  49. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
  50. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
  51. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
  52. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
  53. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
  54. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
  55. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
  56. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
  57. package/extensions/ahp/internal/mapper/title.go +34 -0
  58. package/extensions/ahp/internal/mapper/usermsg.go +87 -0
  59. package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
  60. package/extensions/ahp/internal/pi/backend.go +64 -0
  61. package/extensions/ahp/internal/pi/catalogue.go +370 -0
  62. package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
  63. package/extensions/ahp/internal/pi/chatdriver.go +533 -0
  64. package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
  65. package/extensions/ahp/internal/pi/clientactions.go +212 -0
  66. package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
  67. package/extensions/ahp/internal/pi/completions.go +230 -0
  68. package/extensions/ahp/internal/pi/completions_test.go +272 -0
  69. package/extensions/ahp/internal/pi/config_test.go +269 -0
  70. package/extensions/ahp/internal/pi/deletesession.go +69 -0
  71. package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
  72. package/extensions/ahp/internal/pi/disposal_test.go +362 -0
  73. package/extensions/ahp/internal/pi/fixture_test.go +308 -0
  74. package/extensions/ahp/internal/pi/foreign_test.go +122 -0
  75. package/extensions/ahp/internal/pi/harness_test.go +83 -0
  76. package/extensions/ahp/internal/pi/history.go +269 -0
  77. package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
  78. package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
  79. package/extensions/ahp/internal/pi/hydration_test.go +209 -0
  80. package/extensions/ahp/internal/pi/hydrator.go +212 -0
  81. package/extensions/ahp/internal/pi/imageinput.go +224 -0
  82. package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
  83. package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
  84. package/extensions/ahp/internal/pi/models.go +212 -0
  85. package/extensions/ahp/internal/pi/models_test.go +128 -0
  86. package/extensions/ahp/internal/pi/paging.go +95 -0
  87. package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
  88. package/extensions/ahp/internal/pi/projecttrust.go +222 -0
  89. package/extensions/ahp/internal/pi/registry.go +924 -0
  90. package/extensions/ahp/internal/pi/restart_test.go +185 -0
  91. package/extensions/ahp/internal/pi/services.go +70 -0
  92. package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
  93. package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
  94. package/extensions/ahp/internal/pi/sessionstore.go +83 -0
  95. package/extensions/ahp/internal/pi/summary_test.go +426 -0
  96. package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
  97. package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
  98. package/extensions/ahp/internal/pisession/json.go +31 -0
  99. package/extensions/ahp/internal/pisession/pisession.go +580 -0
  100. package/extensions/ahp/internal/settings/access.go +63 -0
  101. package/extensions/ahp/internal/settings/access_test.go +52 -0
  102. package/extensions/ahp/internal/settings/settings.go +117 -0
  103. package/extensions/ahp/internal/settings/settings_test.go +104 -0
  104. package/extensions/ahp/internal/svc/etag_other.go +15 -0
  105. package/extensions/ahp/internal/svc/etag_unix.go +34 -0
  106. package/extensions/ahp/internal/svc/glob.go +197 -0
  107. package/extensions/ahp/internal/svc/mime.go +37 -0
  108. package/extensions/ahp/internal/svc/paths.go +108 -0
  109. package/extensions/ahp/internal/svc/pty.go +64 -0
  110. package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
  111. package/extensions/ahp/internal/svc/pty_linux.go +36 -0
  112. package/extensions/ahp/internal/svc/pty_other.go +9 -0
  113. package/extensions/ahp/internal/svc/pty_test.go +114 -0
  114. package/extensions/ahp/internal/svc/pty_unix.go +147 -0
  115. package/extensions/ahp/internal/svc/resource.go +495 -0
  116. package/extensions/ahp/internal/svc/resource_test.go +501 -0
  117. package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
  118. package/extensions/ahp/internal/svc/stat_linux.go +9 -0
  119. package/extensions/ahp/internal/svc/terminal.go +454 -0
  120. package/extensions/ahp/internal/svc/terminal_test.go +512 -0
  121. package/extensions/ahp/internal/svc/watch.go +590 -0
  122. package/extensions/ahp/internal/svc/watch_test.go +627 -0
  123. package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
  124. package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
  125. package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
  126. package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
  127. package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
  128. package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
  129. package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
  130. package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
  131. package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
  132. package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
  133. package/extensions/ahp/internal/testkit/schema.go +360 -0
  134. package/extensions/ahp/internal/testkit/testkit.go +367 -0
  135. package/extensions/ahp/internal/twin/twin.go +27 -0
  136. package/extensions/ahp/internal/wire/dispatchable.go +106 -0
  137. package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
  138. package/extensions/ahp/internal/wire/helpers_test.go +5 -0
  139. package/extensions/ahp/internal/wire/wire.go +218 -0
  140. package/extensions/ahp/internal/wire/wire_test.go +123 -0
  141. package/extensions/ahp/internal/ws/helpers_test.go +7 -0
  142. package/extensions/ahp/internal/ws/vscode_test.go +198 -0
  143. package/extensions/ahp/internal/ws/ws.go +674 -0
  144. package/extensions/ahp/internal/ws/ws_test.go +324 -0
  145. package/extensions/ahp/realpig_test.go +589 -0
  146. package/extensions/ahp/runtime.go +311 -0
  147. package/extensions/ahp/runtime_test.go +110 -0
  148. package/extensions/ahp/testdata/upstream-tests.json +3833 -0
  149. package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
  150. package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
  151. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
  152. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
  153. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
  154. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
  155. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
  156. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
  157. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
  158. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
  159. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
  160. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
  161. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
  162. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
  163. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
  164. package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
  165. package/extensions/ahp/twins_test.go +91 -0
  166. package/package.json +40 -0
  167. package/proof/PORT.md +121 -0
  168. package/proof/mutations.json +152 -0
  169. package/proof/oracle/LICENSE +21 -0
  170. package/proof/oracle/README.md +120 -0
  171. package/proof/oracle/UPSTREAM.md +7 -0
  172. package/proof/oracle/package.json +68 -0
  173. package/proof/oracle/src/bin/cli.ts +66 -0
  174. package/proof/oracle/src/bin/tunnel.ts +128 -0
  175. package/proof/oracle/src/channels/chat.ts +134 -0
  176. package/proof/oracle/src/channels/root.ts +39 -0
  177. package/proof/oracle/src/channels/session.ts +118 -0
  178. package/proof/oracle/src/channels/terminal.ts +11 -0
  179. package/proof/oracle/src/core/channels.ts +108 -0
  180. package/proof/oracle/src/core/client-workarounds.ts +358 -0
  181. package/proof/oracle/src/core/connection.ts +41 -0
  182. package/proof/oracle/src/core/host.ts +882 -0
  183. package/proof/oracle/src/core/sequencer.ts +75 -0
  184. package/proof/oracle/src/core/state-store.ts +162 -0
  185. package/proof/oracle/src/core/uri.ts +25 -0
  186. package/proof/oracle/src/host/direct-settings.ts +110 -0
  187. package/proof/oracle/src/host/pi-host.ts +220 -0
  188. package/proof/oracle/src/host/serve.ts +71 -0
  189. package/proof/oracle/src/host/terminal-service.ts +346 -0
  190. package/proof/oracle/src/pi/activity.ts +171 -0
  191. package/proof/oracle/src/pi/changeset-service.ts +646 -0
  192. package/proof/oracle/src/pi/changeset-uri.ts +64 -0
  193. package/proof/oracle/src/pi/chat-driver.ts +529 -0
  194. package/proof/oracle/src/pi/completions.ts +160 -0
  195. package/proof/oracle/src/pi/delete-session.ts +53 -0
  196. package/proof/oracle/src/pi/event-mapper.ts +648 -0
  197. package/proof/oracle/src/pi/git-changes.ts +605 -0
  198. package/proof/oracle/src/pi/history.ts +305 -0
  199. package/proof/oracle/src/pi/image-input.ts +56 -0
  200. package/proof/oracle/src/pi/image-mime.ts +8 -0
  201. package/proof/oracle/src/pi/in-process-backend.ts +155 -0
  202. package/proof/oracle/src/pi/message-input.ts +212 -0
  203. package/proof/oracle/src/pi/models.ts +124 -0
  204. package/proof/oracle/src/pi/project-trust.ts +66 -0
  205. package/proof/oracle/src/pi/provider.ts +2 -0
  206. package/proof/oracle/src/pi/resource-paths.ts +80 -0
  207. package/proof/oracle/src/pi/resource-service.ts +356 -0
  208. package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
  209. package/proof/oracle/src/pi/resource-watch.ts +357 -0
  210. package/proof/oracle/src/pi/session-catalogue.ts +318 -0
  211. package/proof/oracle/src/pi/session-config.ts +84 -0
  212. package/proof/oracle/src/pi/session-history.ts +75 -0
  213. package/proof/oracle/src/pi/session-hydrator.ts +219 -0
  214. package/proof/oracle/src/pi/session-registry.ts +815 -0
  215. package/proof/oracle/src/pi/session-storage.ts +29 -0
  216. package/proof/oracle/src/pi/session-title.ts +18 -0
  217. package/proof/oracle/src/pi/turn-paging.ts +94 -0
  218. package/proof/oracle/src/pi/user-message.ts +66 -0
  219. package/proof/oracle/src/protocol/errors.ts +44 -0
  220. package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
  221. package/proof/oracle/src/protocol/version.ts +39 -0
  222. package/proof/oracle/src/transport/websocket.ts +136 -0
  223. package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
  224. package/proof/oracle/src/tunnel/discovery.ts +37 -0
  225. package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
  226. package/proof/oracle/test/activity.test.ts +171 -0
  227. package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
  228. package/proof/oracle/test/changeset-uri.test.ts +37 -0
  229. package/proof/oracle/test/changeset.test.serial.ts +487 -0
  230. package/proof/oracle/test/chat-driver.test.ts +691 -0
  231. package/proof/oracle/test/client-actions.test.ts +409 -0
  232. package/proof/oracle/test/client-workarounds.test.ts +320 -0
  233. package/proof/oracle/test/completions.test.ts +323 -0
  234. package/proof/oracle/test/delete-session.test.ts +142 -0
  235. package/proof/oracle/test/direct-settings.test.ts +81 -0
  236. package/proof/oracle/test/event-mapper.test.ts +619 -0
  237. package/proof/oracle/test/fetch-turns.test.ts +207 -0
  238. package/proof/oracle/test/fixtures/abort.json +711 -0
  239. package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
  240. package/proof/oracle/test/fixtures/compaction.json +6285 -0
  241. package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
  242. package/proof/oracle/test/fixtures/plain-text.json +1856 -0
  243. package/proof/oracle/test/fixtures/single-tool.json +1128 -0
  244. package/proof/oracle/test/fixtures/steering.json +3926 -0
  245. package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
  246. package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
  247. package/proof/oracle/test/fixtures/tool-error.json +2850 -0
  248. package/proof/oracle/test/fixtures/tool-find.json +1162 -0
  249. package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
  250. package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
  251. package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
  252. package/proof/oracle/test/fixtures/tool-write.json +1271 -0
  253. package/proof/oracle/test/handshake.test.ts +275 -0
  254. package/proof/oracle/test/harness.ts +153 -0
  255. package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
  256. package/proof/oracle/test/image-input.test.ts +28 -0
  257. package/proof/oracle/test/image-session.test.ts +69 -0
  258. package/proof/oracle/test/live-turn.test.ts +304 -0
  259. package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
  260. package/proof/oracle/test/message-input.test.ts +181 -0
  261. package/proof/oracle/test/model-discovery.test.ts +170 -0
  262. package/proof/oracle/test/models.test.ts +109 -0
  263. package/proof/oracle/test/pi-host.test.ts +91 -0
  264. package/proof/oracle/test/pi-replay.test.ts +141 -0
  265. package/proof/oracle/test/project-trust.test.ts +125 -0
  266. package/proof/oracle/test/protocol-surface.test.ts +121 -0
  267. package/proof/oracle/test/pty.test.ts +100 -0
  268. package/proof/oracle/test/reconnect.test.ts +389 -0
  269. package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
  270. package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
  271. package/proof/oracle/test/resource.test.ts +499 -0
  272. package/proof/oracle/test/schema.test.ts +88 -0
  273. package/proof/oracle/test/session-catalogue.test.ts +314 -0
  274. package/proof/oracle/test/session-config.test.ts +224 -0
  275. package/proof/oracle/test/session-disposal.test.ts +364 -0
  276. package/proof/oracle/test/session-hydration.test.ts +194 -0
  277. package/proof/oracle/test/session-lifecycle.test.ts +416 -0
  278. package/proof/oracle/test/session-storage.test.serial.ts +32 -0
  279. package/proof/oracle/test/session-summary.test.ts +418 -0
  280. package/proof/oracle/test/subscriptions.test.ts +198 -0
  281. package/proof/oracle/test/support/assertions.ts +32 -0
  282. package/proof/oracle/test/support/async.ts +26 -0
  283. package/proof/oracle/test/support/hydrated-session.ts +205 -0
  284. package/proof/oracle/test/support/images.ts +8 -0
  285. package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
  286. package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
  287. package/proof/oracle/test/support/replay.ts +194 -0
  288. package/proof/oracle/test/support/schema.ts +221 -0
  289. package/proof/oracle/test/support/session-files.ts +12 -0
  290. package/proof/oracle/test/support/session-storage.ts +22 -0
  291. package/proof/oracle/test/support/upstream.ts +25 -0
  292. package/proof/oracle/test/support/watch-events.ts +108 -0
  293. package/proof/oracle/test/terminal-service.test.ts +495 -0
  294. package/proof/oracle/test/truncate.test.ts +230 -0
  295. package/proof/oracle/test/tunnel.test.ts +258 -0
  296. package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
  297. package/proof/oracle/test/uri.test.ts +24 -0
  298. package/proof/oracle/test/watch-events.test.ts +118 -0
  299. package/proof/oracle/tsconfig.json +32 -0
  300. package/proof/proof-piglet/piglet.yaml +18 -0
  301. package/proof/tools/fakellm/go.mod +3 -0
  302. package/proof/tools/fakellm/main.go +109 -0
  303. package/proof/tools/gen-dispatchable.py +30 -0
  304. package/provenance.json +28 -0
@@ -0,0 +1,320 @@
1
+ /** Per-connection URI compatibility without leaking client dialects into core state. */
2
+
3
+ import assert from "node:assert/strict";
4
+ import { randomUUID } from "node:crypto";
5
+ import { after, before, describe, it } from "node:test";
6
+ import {
7
+ ActionType,
8
+ type ChatState,
9
+ JsonRpcErrorCodes,
10
+ MessageKind,
11
+ type SessionState,
12
+ SUPPORTED_PROTOCOL_VERSIONS,
13
+ } from "@microsoft/agent-host-protocol";
14
+ import { AhpClient, RpcError } from "@microsoft/agent-host-protocol/client";
15
+ import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
16
+ import { chatUri, ROOT_CHANNEL, sessionIdFromUri, sessionUri } from "../src/core/channels.ts";
17
+ import { ClientWorkarounds } from "../src/core/client-workarounds.ts";
18
+ import { PI_PROVIDER } from "../src/pi/provider.ts";
19
+ import type { RunningServer } from "../src/transport/websocket.ts";
20
+ import { expectRpcError, must } from "./support/assertions.ts";
21
+ import { type HydratedSessionFixture, startHydratedSessionFixture } from "./support/hydrated-session.ts";
22
+ import { assertValid } from "./support/schema.ts";
23
+
24
+ async function expectVscodeDisposalRefusal(request: Promise<unknown>): Promise<void> {
25
+ const error = await expectRpcError(request, JsonRpcErrorCodes.InvalidRequest);
26
+ assert.match(error.message, /temporarily disabled for VS Code/u);
27
+ assert.match(error.message, /VS Code provisional-session lifecycle bug/u);
28
+ assert.match(error.message, /session was kept/u);
29
+ }
30
+
31
+ async function initialSnapshotResources(
32
+ server: RunningServer,
33
+ clientId: string,
34
+ initialSubscriptions: string[],
35
+ clientInfo?: { name: string },
36
+ ): Promise<string[]> {
37
+ const client = new AhpClient(await WebSocketTransport.connect(`ws://127.0.0.1:${server.port}`));
38
+ client.connect();
39
+ try {
40
+ const result = await client.request("initialize", {
41
+ channel: ROOT_CHANNEL,
42
+ clientId,
43
+ protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
44
+ initialSubscriptions,
45
+ ...(clientInfo ? { clientInfo } : {}),
46
+ });
47
+ return result.snapshots.map((snapshot) => snapshot.resource);
48
+ } finally {
49
+ await client.shutdown();
50
+ }
51
+ }
52
+
53
+ describe("client URI dialects over the wire", () => {
54
+ let fixture: HydratedSessionFixture;
55
+
56
+ before(async () => {
57
+ fixture = await startHydratedSessionFixture();
58
+ });
59
+
60
+ after(async () => {
61
+ await fixture.close();
62
+ });
63
+
64
+ it("answers VS Code at the URIs it computes for itself", async () => {
65
+ const client = await fixture.connectAsVSCode();
66
+ const providerSession = `pi:/${fixture.sessionId}`;
67
+ const derived = `ahp-chat://default/${Buffer.from(providerSession).toString("base64url")}`;
68
+
69
+ const session = await client.subscribe(providerSession);
70
+ const sessionState = session.result.snapshot?.state as SessionState;
71
+ assert.equal(session.result.snapshot?.resource, providerSession);
72
+ assert.equal(sessionState.defaultChat, derived);
73
+
74
+ const { result } = await client.subscribe(derived);
75
+ const state = must(result.snapshot).state as ChatState;
76
+ assert.equal(result.snapshot?.resource, derived);
77
+ assert.ok(state.turns.length > 0, "the transcript must come back, not an empty chat");
78
+ assertValid("state", "ChatState", state);
79
+ });
80
+
81
+ for (const [name, clientInfo, usesDerivedChat] of [
82
+ ["VS Code", { name: "vscode-editor-window" }, true],
83
+ ["unnamed provider-alias client", undefined, false],
84
+ ] as const) {
85
+ it(`applies the ${name} dialect to initialize-time subscriptions`, async () => {
86
+ const providerSession = `pi:/${fixture.sessionId}`;
87
+ const chat = usesDerivedChat
88
+ ? `ahp-chat://default/${Buffer.from(providerSession).toString("base64url")}`
89
+ : chatUri(fixture.sessionId);
90
+ const subscriptions = [providerSession, chat];
91
+
92
+ assert.deepEqual(
93
+ await initialSnapshotResources(fixture.server, `initial-subscriptions-${name}`, subscriptions, clientInfo),
94
+ subscriptions,
95
+ );
96
+ });
97
+ }
98
+ });
99
+
100
+ describe("VS Code session disposal workaround", () => {
101
+ it("protects a durable session that this connection did not create", async () => {
102
+ const fixture = await startHydratedSessionFixture();
103
+ try {
104
+ const client = await fixture.connectAsVSCode();
105
+ const resource = `pi:/${fixture.sessionId}`;
106
+
107
+ await expectVscodeDisposalRefusal(client.request("disposeSession", { channel: resource }));
108
+
109
+ assert.deepEqual(fixture.deletedFiles, []);
110
+ const listed = await client.request("listSessions", { channel: ROOT_CHANNEL });
111
+ assert.equal(
112
+ listed.items.some((item) => item.resource === resource),
113
+ true,
114
+ );
115
+ } finally {
116
+ await fixture.close();
117
+ }
118
+ });
119
+
120
+ it("still disposes an empty session created by this VS Code connection", async () => {
121
+ const fixture = await startHydratedSessionFixture();
122
+ try {
123
+ const client = await fixture.connectAsVSCode();
124
+ const id = randomUUID();
125
+ const resource = `pi:/${id}`;
126
+ await client.request("createSession", { channel: resource });
127
+
128
+ await client.request("disposeSession", { channel: resource });
129
+
130
+ assert.equal(fixture.host.store.has(sessionUri(id)), false);
131
+ } finally {
132
+ await fixture.close();
133
+ }
134
+ });
135
+
136
+ it("restores an empty session after disposal fails so VS Code can retry", async () => {
137
+ let attempts = 0;
138
+ const fixture = await startHydratedSessionFixture({
139
+ deleteFile: () => {
140
+ attempts += 1;
141
+ return attempts === 1 ? { ok: false, error: "temporary failure" } : { ok: true };
142
+ },
143
+ });
144
+ try {
145
+ const client = await fixture.connectAsVSCode();
146
+ const id = randomUUID();
147
+ const resource = `pi:/${id}`;
148
+ await client.request("createSession", { channel: resource });
149
+
150
+ const error = await expectRpcError(
151
+ client.request("disposeSession", { channel: resource }),
152
+ JsonRpcErrorCodes.InternalError,
153
+ );
154
+ assert.match(error.message, /temporary failure/u);
155
+ await client.request("disposeSession", { channel: resource });
156
+
157
+ assert.equal(attempts, 2);
158
+ assert.equal(fixture.host.store.has(sessionUri(id)), false);
159
+ } finally {
160
+ await fixture.close();
161
+ }
162
+ });
163
+
164
+ it("protects a session after its first turn starts", async () => {
165
+ const fixture = await startHydratedSessionFixture();
166
+ try {
167
+ const client = await fixture.connectAsVSCode();
168
+ const id = randomUUID();
169
+ const resource = `pi:/${id}`;
170
+ const chat = `ahp-chat://default/${Buffer.from(resource).toString("base64url")}`;
171
+ await client.request("createSession", { channel: resource });
172
+ client.dispatch(chat, {
173
+ type: ActionType.ChatTurnStarted,
174
+ turnId: "materialized-turn",
175
+ startedAt: new Date().toISOString(),
176
+ message: { text: "keep this", origin: { kind: MessageKind.User } },
177
+ });
178
+
179
+ await expectVscodeDisposalRefusal(client.request("disposeSession", { channel: resource }));
180
+
181
+ assert.equal(fixture.host.store.has(sessionUri(id)), true);
182
+ } finally {
183
+ await fixture.close();
184
+ }
185
+ });
186
+ });
187
+
188
+ describe("session URI classification", () => {
189
+ it("recognises only canonical or explicitly allowed session schemes", () => {
190
+ const id = "9991C40A-74CC-4991-85CC-F37CD2BFF065";
191
+ assert.equal(sessionIdFromUri(`pi:/${id}`, [PI_PROVIDER]), id);
192
+ assert.equal(sessionIdFromUri("ahp-session:/abc"), "abc");
193
+ assert.equal(sessionIdFromUri("copilot:/test-session"), undefined);
194
+ assert.equal(sessionIdFromUri("copilot:/test-session", ["copilot"]), "test-session");
195
+ assert.equal(sessionIdFromUri("pi://test-session", [PI_PROVIDER]), undefined);
196
+ });
197
+
198
+ it("does not infer sessions from other channel schemes", () => {
199
+ for (const uri of ["ahp-chat:/c1", "ahp-terminal:/t1", "agenthost-terminal:/t1", "file:///etc/passwd"]) {
200
+ assert.equal(sessionIdFromUri(uri, [PI_PROVIDER]), undefined, uri);
201
+ }
202
+ });
203
+
204
+ it("retargets VS Code's chat-addressed rename to the owning session", () => {
205
+ const id = "rename-me";
206
+ const workarounds = new ClientWorkarounds();
207
+ workarounds.identify({ name: "vscode-editor-window" });
208
+ const message = {
209
+ jsonrpc: "2.0" as const,
210
+ method: "dispatchAction",
211
+ params: {
212
+ channel: `ahp-chat://default/${Buffer.from(`pi:/${id}`).toString("base64url")}`,
213
+ clientSeq: 1,
214
+ action: { type: ActionType.SessionTitleChanged, title: "Renamed" },
215
+ },
216
+ };
217
+
218
+ workarounds.applyToIncoming(message);
219
+
220
+ assert.equal(message.params.channel, sessionUri(id));
221
+ });
222
+
223
+ it("rewrites URI-bearing action and catalogue fields for VS Code", () => {
224
+ const id = "nested-fields";
225
+ const session = sessionUri(id);
226
+ const chat = chatUri(id);
227
+ const providerSession = `pi:/${id}`;
228
+ const derivedChat = `ahp-chat://default/${Buffer.from(providerSession).toString("base64url")}`;
229
+ const workarounds = new ClientWorkarounds();
230
+ workarounds.identify({ name: "vscode-editor-window" });
231
+
232
+ const removed = workarounds.applyToOutgoing({
233
+ jsonrpc: "2.0",
234
+ method: "root/sessionRemoved",
235
+ params: { channel: ROOT_CHANNEL, session },
236
+ });
237
+ assert.deepEqual(removed, {
238
+ jsonrpc: "2.0",
239
+ method: "root/sessionRemoved",
240
+ params: { channel: ROOT_CHANNEL, session: providerSession },
241
+ });
242
+
243
+ const updated = workarounds.applyToOutgoing({
244
+ jsonrpc: "2.0",
245
+ method: "action",
246
+ params: {
247
+ channel: session,
248
+ action: { type: ActionType.SessionChatUpdated, chat, changes: { resource: chat } },
249
+ serverSeq: 1,
250
+ },
251
+ });
252
+ assert.deepEqual(updated, {
253
+ jsonrpc: "2.0",
254
+ method: "action",
255
+ params: {
256
+ channel: providerSession,
257
+ action: {
258
+ type: ActionType.SessionChatUpdated,
259
+ chat: derivedChat,
260
+ changes: { resource: derivedChat },
261
+ },
262
+ serverSeq: 1,
263
+ },
264
+ });
265
+ });
266
+
267
+ it("leaves VS Code's client-chosen terminal URI outside session translation", () => {
268
+ const channel = "agenthost-terminal:/terminal-1";
269
+ const workarounds = new ClientWorkarounds();
270
+ workarounds.identify({ name: "vscode-editor-window" });
271
+
272
+ const message = { jsonrpc: "2.0" as const, method: "action", params: { channel } };
273
+ assert.deepEqual(workarounds.applyToOutgoing(message), message);
274
+ });
275
+
276
+ it("does not hydrate or dispose a durable session through VS Code's terminal URI", async () => {
277
+ const fixture = await startHydratedSessionFixture();
278
+ try {
279
+ const client = await fixture.connectAsVSCode();
280
+ const channel = `agenthost-terminal:/${fixture.sessionId}`;
281
+ await assert.rejects(
282
+ client.subscribe(channel),
283
+ RpcError,
284
+ "an uncreated terminal must not open a matching session",
285
+ );
286
+ await assert.rejects(
287
+ client.request("disposeSession", { channel }),
288
+ RpcError,
289
+ "a terminal URI must not delete a matching session",
290
+ );
291
+ assert.equal(fixture.host.store.has(channel), false);
292
+ assert.deepEqual(fixture.deletedFiles, []);
293
+ } finally {
294
+ await fixture.close();
295
+ }
296
+ });
297
+
298
+ it("accepts the provider scheme but rejects an undeclared session scheme", async () => {
299
+ const fixture = await startHydratedSessionFixture();
300
+ try {
301
+ await assert.rejects(fixture.client.request("createSession", { channel: `custom:/${randomUUID()}` }), RpcError);
302
+
303
+ const id = randomUUID().toUpperCase();
304
+ const uri = `pi:/${id}`;
305
+ await fixture.client.request("createSession", { channel: uri });
306
+
307
+ const { result } = await fixture.client.subscribe(uri);
308
+ assert.ok(result.snapshot, "the session must exist at the URI the client chose");
309
+ assert.equal(result.snapshot.resource, uri);
310
+ assert.equal((result.snapshot.state as SessionState).defaultChat, chatUri(id));
311
+ assert.equal(fixture.host.store.has(sessionUri(id)), true, "core state stays canonical");
312
+ assert.equal(fixture.host.store.has(uri), false);
313
+
314
+ await fixture.client.request("disposeSession", { channel: uri });
315
+ assert.equal(fixture.host.store.has(sessionUri(id)), false);
316
+ } finally {
317
+ await fixture.close();
318
+ }
319
+ });
320
+ });
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Inline completions for a message being composed.
3
+ *
4
+ * The protocol makes `CompletionItem.attachment` required, which decides what
5
+ * this feature is: an attachment picker. `@`-mentions fit; pi's slash commands
6
+ * do not, because they attach nothing.
7
+ *
8
+ * @see https://microsoft.github.io/agent-host-protocol/specification/chat-channel
9
+ */
10
+
11
+ import assert from "node:assert/strict";
12
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { after, before, describe, it } from "node:test";
16
+ import {
17
+ CompletionItemKind,
18
+ type CompletionsResult,
19
+ type InitializeResult,
20
+ JsonRpcErrorCodes,
21
+ MessageAttachmentKind,
22
+ SUPPORTED_PROTOCOL_VERSIONS,
23
+ } from "@microsoft/agent-host-protocol";
24
+ import { AhpClient, RpcError } from "@microsoft/agent-host-protocol/client";
25
+ import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
26
+ import { installRootChannel } from "../src/channels/root.ts";
27
+ import { AhpHost } from "../src/core/host.ts";
28
+ import { CompletionService, findMention, MENTION_TRIGGER } from "../src/pi/completions.ts";
29
+ import { type RunningServer, serveWebSocket } from "../src/transport/websocket.ts";
30
+ import { assertValid } from "./support/schema.ts";
31
+
32
+ const CHAT = "ahp-chat:/completions";
33
+
34
+ describe("mention parsing", () => {
35
+ it("finds a mention the cursor sits in", () => {
36
+ const found = findMention("look at @foo", 12);
37
+ assert.deepEqual(found, { start: 8, end: 12, query: "foo" });
38
+ });
39
+
40
+ it("finds a bare mention at the start of the text", () => {
41
+ assert.equal(findMention("@src", 4)?.query, "src");
42
+ });
43
+
44
+ it("ignores an `@` that is not preceded by whitespace", () => {
45
+ // Otherwise every email address and decorator opens the picker.
46
+ assert.equal(findMention("mail me at a@b.com", 18), undefined);
47
+ });
48
+
49
+ it("ignores a cursor that is past the end of the mention", () => {
50
+ assert.equal(findMention("@foo bar", 8), undefined);
51
+ });
52
+
53
+ it("returns the empty query for a lone trigger", () => {
54
+ assert.equal(findMention("@", 1)?.query, "");
55
+ });
56
+ });
57
+
58
+ describe("completions", () => {
59
+ let workspace: string;
60
+ let service: CompletionService;
61
+
62
+ before(() => {
63
+ workspace = mkdtempSync(join(tmpdir(), "pi-ahp-completions-"));
64
+ writeFileSync(join(workspace, "readme.md"), "x");
65
+ writeFileSync(join(workspace, "report.txt"), "x");
66
+ writeFileSync(join(workspace, "other.js"), "x");
67
+ writeFileSync(join(workspace, ".hidden"), "x");
68
+ mkdirSync(join(workspace, "src", "lib"), { recursive: true });
69
+ writeFileSync(join(workspace, "src", "index.ts"), "x");
70
+ writeFileSync(join(workspace, "src", "lib", "deep.ts"), "x");
71
+ mkdirSync(join(workspace, "node_modules"));
72
+ writeFileSync(join(workspace, "node_modules", "reactive.js"), "x");
73
+ mkdirSync(join(workspace, "dist"));
74
+ writeFileSync(join(workspace, "dist", "generated.js"), "x");
75
+ mkdirSync(join(workspace, "build"));
76
+ writeFileSync(join(workspace, "build", "artifact.js"), "x");
77
+
78
+ service = new CompletionService({ workingDirectoryFor: () => workspace });
79
+ });
80
+
81
+ after(() => {
82
+ rmSync(workspace, { recursive: true, force: true });
83
+ });
84
+
85
+ const complete = (text: string, offset = text.length): Promise<CompletionsResult> =>
86
+ service.complete({ kind: CompletionItemKind.UserMessage, channel: CHAT, text, offset });
87
+
88
+ it("offers files matching the typed prefix", async () => {
89
+ const { items } = await complete("look at @re");
90
+
91
+ const inserted = items.map((item) => item.insertText);
92
+ assert.deepEqual(inserted.sort(), ["@readme.md", "@report.txt"]);
93
+ });
94
+
95
+ it("attaches the resource each item refers to", async () => {
96
+ const { items } = await complete("@readme");
97
+ const item = items[0];
98
+
99
+ assert.ok(item);
100
+ assert.equal(item.attachment.type, MessageAttachmentKind.Resource);
101
+ assert.equal(item.attachment.label, "readme.md");
102
+ assert.match((item.attachment as { uri: string }).uri, /readme\.md$/);
103
+ assertValid("commands", "CompletionItem", item);
104
+ });
105
+
106
+ it("marks the range the client should replace", async () => {
107
+ const { items } = await complete("look at @re");
108
+
109
+ // The span covers the `@` through the cursor, so accepting replaces the
110
+ // half-typed mention rather than appending to it.
111
+ assert.equal(items[0]?.rangeStart, 8);
112
+ assert.equal(items[0]?.rangeEnd, 11);
113
+ });
114
+
115
+ it("returns nested files instead of directory navigation items", async () => {
116
+ const { items } = await complete("@sr");
117
+
118
+ assert.deepEqual(
119
+ items.map((item) => item.insertText),
120
+ ["@src/index.ts", "@src/lib/deep.ts"],
121
+ );
122
+ assert.ok(items.every((item) => item.attachment.displayKind === "document"));
123
+ });
124
+
125
+ it("matches a typed relative-path prefix", async () => {
126
+ const { items } = await complete("@src/in");
127
+
128
+ assert.deepEqual(
129
+ items.map((item) => item.insertText),
130
+ ["@src/index.ts"],
131
+ );
132
+ });
133
+
134
+ it("matches a nested file by basename", async () => {
135
+ const { items } = await complete("@deep");
136
+
137
+ assert.deepEqual(
138
+ items.map((item) => item.insertText),
139
+ ["@src/lib/deep.ts"],
140
+ );
141
+ });
142
+
143
+ it("keeps the temporary directory skip list narrow", async () => {
144
+ const inserted = (await complete("@")).items.map((item) => item.insertText);
145
+
146
+ assert.equal(
147
+ inserted.some((item) => item.includes("node_modules")),
148
+ false,
149
+ );
150
+ assert.equal(
151
+ inserted.some((item) => item.includes(".hidden")),
152
+ false,
153
+ );
154
+ assert.ok(inserted.includes("@dist/generated.js"));
155
+ assert.ok(inserted.includes("@build/artifact.js"));
156
+ });
157
+
158
+ it("does not interpret a query as path traversal", async () => {
159
+ const { items } = await complete("@../../etc/pass");
160
+ assert.deepEqual(items, []);
161
+ });
162
+
163
+ it("returns nothing for a completion kind it does not implement", async () => {
164
+ const result = await service.complete({
165
+ kind: "futureKind" as CompletionItemKind,
166
+ channel: CHAT,
167
+ text: "@not",
168
+ offset: 4,
169
+ });
170
+ assert.deepEqual(result.items, []);
171
+ });
172
+
173
+ it("returns nothing when the cursor is not in a mention", async () => {
174
+ assert.deepEqual((await complete("plain text")).items, []);
175
+ });
176
+
177
+ it("returns nothing for a chat it has no directory for", async () => {
178
+ const orphan = new CompletionService({ workingDirectoryFor: () => undefined });
179
+ const result = await orphan.complete({
180
+ kind: CompletionItemKind.UserMessage,
181
+ channel: CHAT,
182
+ text: "@re",
183
+ offset: 3,
184
+ });
185
+
186
+ assert.deepEqual(result.items, []);
187
+ });
188
+
189
+ it("caps how many items it returns", async () => {
190
+ const crowded = mkdtempSync(join(tmpdir(), "pi-ahp-crowded-"));
191
+ try {
192
+ for (let i = 0; i < 40; i++) {
193
+ writeFileSync(join(crowded, `file-${i}.txt`), "x");
194
+ }
195
+ const capped = new CompletionService({ workingDirectoryFor: () => crowded, maxItems: 5 });
196
+ const result = await capped.complete({
197
+ kind: CompletionItemKind.UserMessage,
198
+ channel: CHAT,
199
+ text: "@file",
200
+ offset: 5,
201
+ });
202
+
203
+ assert.equal(result.items.length, 5);
204
+ } finally {
205
+ rmSync(crowded, { recursive: true, force: true });
206
+ }
207
+ });
208
+ });
209
+
210
+ describe("completions over the wire", () => {
211
+ let server: RunningServer;
212
+ let client: AhpClient;
213
+ let initialization: InitializeResult;
214
+ let workspace: string;
215
+
216
+ before(async () => {
217
+ workspace = mkdtempSync(join(tmpdir(), "pi-ahp-completions-wire-"));
218
+ writeFileSync(join(workspace, "notes.md"), "x");
219
+
220
+ const host = new AhpHost({ completionTriggerCharacters: [MENTION_TRIGGER] });
221
+ installRootChannel(host, []);
222
+ host.serve({
223
+ completions: new CompletionService({
224
+ workingDirectoryFor: (channel) => (channel === CHAT ? workspace : undefined),
225
+ }),
226
+ });
227
+
228
+ server = await serveWebSocket(host, { host: "127.0.0.1", port: 0 });
229
+ client = new AhpClient(await WebSocketTransport.connect(`ws://127.0.0.1:${server.port}`));
230
+ client.connect();
231
+ initialization = await client.initialize({
232
+ clientId: "completions-client",
233
+ protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
234
+ });
235
+ });
236
+
237
+ after(async () => {
238
+ await client.shutdown();
239
+ await server.close();
240
+ rmSync(workspace, { recursive: true, force: true });
241
+ });
242
+
243
+ it("advertises only the trigger it can answer", () => {
244
+ // `/` is deliberately absent: pi's slash commands attach nothing, and
245
+ // every completion item must carry an attachment.
246
+ assert.deepEqual(initialization.completionTriggerCharacters, ["@"]);
247
+ assertValid("commands", "InitializeResult", initialization);
248
+ });
249
+
250
+ it("serves a schema-conforming result", async () => {
251
+ const result = await client.request("completions", {
252
+ kind: CompletionItemKind.UserMessage,
253
+ channel: CHAT,
254
+ text: "see @not",
255
+ offset: 8,
256
+ });
257
+
258
+ assert.equal(result.items.length, 1);
259
+ assert.equal(result.items[0]?.insertText, "@notes.md");
260
+ assertValid("commands", "CompletionsResult", result);
261
+ });
262
+
263
+ it("rejects non-chat completion targets", async () => {
264
+ const params = { kind: CompletionItemKind.UserMessage, text: "@not", offset: 4 };
265
+ for (const channel of [undefined, "ahp-session:/completions"]) {
266
+ await assert.rejects(
267
+ client.request("completions", { ...params, ...(channel ? { channel } : {}) } as never),
268
+ (error: unknown) => error instanceof RpcError && error.code === JsonRpcErrorCodes.InvalidParams,
269
+ );
270
+ }
271
+ });
272
+
273
+ it("maps provider-alias completion targets onto the default chat", async () => {
274
+ const vscode = new AhpClient(await WebSocketTransport.connect(`ws://127.0.0.1:${server.port}`));
275
+ vscode.connect();
276
+ await vscode.request("initialize", {
277
+ channel: "ahp-root://",
278
+ clientId: "vscode-completions-client",
279
+ clientInfo: { name: "vscode-editor-window" },
280
+ protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
281
+ });
282
+ try {
283
+ const params = { kind: CompletionItemKind.UserMessage, text: "@not", offset: 4 };
284
+ for (const result of await Promise.all([
285
+ vscode.request("completions", { ...params, channel: "pi:/completions" }),
286
+ client.request("completions", { ...params, channel: "pi:/completions" }),
287
+ ])) {
288
+ assert.deepEqual(
289
+ result.items.map((item) => item.insertText),
290
+ ["@notes.md"],
291
+ );
292
+ }
293
+ } finally {
294
+ await vscode.shutdown();
295
+ }
296
+ });
297
+ });
298
+
299
+ describe("completions without a handler", () => {
300
+ it("answers with an empty list rather than MethodNotFound", async () => {
301
+ // The client debounces keystrokes into this call; an error per keypress
302
+ // would be worse than nothing to show.
303
+ const host = new AhpHost();
304
+ installRootChannel(host, []);
305
+ const server = await serveWebSocket(host, { host: "127.0.0.1", port: 0 });
306
+ const client = new AhpClient(await WebSocketTransport.connect(`ws://127.0.0.1:${server.port}`));
307
+ client.connect();
308
+ await client.initialize({ clientId: "bare-client", protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
309
+
310
+ try {
311
+ const result = await client.request("completions", {
312
+ kind: CompletionItemKind.UserMessage,
313
+ channel: CHAT,
314
+ text: "@x",
315
+ offset: 2,
316
+ });
317
+ assert.deepEqual(result.items, []);
318
+ } finally {
319
+ await client.shutdown();
320
+ await server.close();
321
+ }
322
+ });
323
+ });