saeeol 1.3.0 → 1.3.1

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 (537) hide show
  1. package/.turbo/turbo-typecheck.log +1 -0
  2. package/AGENTS.md +72 -0
  3. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  4. package/Dockerfile +18 -0
  5. package/assets/saeeol.ico +0 -0
  6. package/bin/saeeol.cjs +0 -0
  7. package/database.db +0 -0
  8. package/drizzle.config.ts +10 -0
  9. package/git +0 -0
  10. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  11. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  12. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  13. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  14. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  15. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  16. package/migration/20260225215848_workspace/migration.sql +7 -0
  17. package/migration/20260225215848_workspace/snapshot.json +959 -0
  18. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  19. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  20. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  21. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  22. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  23. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  24. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  25. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  26. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  27. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  28. package/migration/20260323234822_events/migration.sql +13 -0
  29. package/migration/20260323234822_events/snapshot.json +1271 -0
  30. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  31. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  32. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  33. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  34. package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
  35. package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
  36. package/migration/20260428004200_add_session_path/migration.sql +1 -0
  37. package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
  38. package/npm/bin/saeeol +42 -0
  39. package/npm/package.json +39 -0
  40. package/npm/postinstall.js +162 -0
  41. package/package.json +201 -207
  42. package/parsers-config.ts +289 -0
  43. package/script/build.ts +393 -0
  44. package/script/check-migrations.ts +16 -0
  45. package/script/fix-node-pty.ts +34 -0
  46. package/script/generate.ts +23 -0
  47. package/script/postinstall.mjs +189 -0
  48. package/script/publish.ts +200 -0
  49. package/script/run-workspace-server +106 -0
  50. package/script/schema.ts +63 -0
  51. package/script/test-runner.ts +420 -0
  52. package/script/time.ts +6 -0
  53. package/script/trace-imports.ts +153 -0
  54. package/script/upgrade-opentui.ts +64 -0
  55. package/scripts/diff-sdk-types.sh +52 -0
  56. package/specs/effect/facades.md +221 -0
  57. package/specs/effect/http-api.md +401 -0
  58. package/specs/effect/instance-context.md +309 -0
  59. package/specs/effect/loose-ends.md +34 -0
  60. package/specs/effect/migration.md +299 -0
  61. package/specs/effect/routes.md +64 -0
  62. package/specs/effect/schema.md +399 -0
  63. package/specs/effect/server-package.md +668 -0
  64. package/specs/effect/tools.md +90 -0
  65. package/specs/tui-plugins.md +433 -0
  66. package/specs/v2/api.ts +67 -0
  67. package/specs/v2/keymappings.md +10 -0
  68. package/specs/v2/message-shape.md +136 -0
  69. package/src/acp/agent-message.ts +1 -1
  70. package/src/acp/agent-utils.ts +1 -1
  71. package/src/boxes/ansi.ts +17 -0
  72. package/src/boxes/atomic-write.ts +35 -0
  73. package/src/boxes/b64.ts +58 -0
  74. package/src/boxes/bash-security.ts +129 -0
  75. package/src/boxes/bom.ts +18 -0
  76. package/src/boxes/cancel.ts +16 -0
  77. package/src/boxes/chop.ts +12 -0
  78. package/src/boxes/clamp.ts +3 -0
  79. package/src/boxes/compact.ts +9 -0
  80. package/src/boxes/cost-tracker.ts +116 -0
  81. package/src/boxes/dataurl.ts +29 -0
  82. package/src/boxes/delay.ts +27 -0
  83. package/src/boxes/diff-apply.ts +53 -0
  84. package/src/boxes/disposable.ts +13 -0
  85. package/src/boxes/err.ts +34 -0
  86. package/src/boxes/human.ts +47 -0
  87. package/src/boxes/iife.ts +9 -0
  88. package/src/boxes/latch.ts +8 -0
  89. package/src/boxes/memory.ts +198 -0
  90. package/src/boxes/net.ts +16 -0
  91. package/src/boxes/plural.ts +4 -0
  92. package/src/boxes/puny.ts +21 -0
  93. package/src/boxes/retry.ts +49 -0
  94. package/src/boxes/rwlock.ts +41 -0
  95. package/src/boxes/schedule.ts +71 -0
  96. package/src/boxes/scope.ts +21 -0
  97. package/src/boxes/tokens.ts +9 -0
  98. package/src/boxes/ttl-cache.ts +63 -0
  99. package/src/boxes/typed-event.ts +51 -0
  100. package/src/boxes/uid.ts +50 -0
  101. package/src/boxes/wave6.test.ts +296 -0
  102. package/src/boxes/wildcard.ts +58 -0
  103. package/src/bus/global.ts +1 -1
  104. package/src/cli/cmd/github-run-api.ts +2 -2
  105. package/src/cli/cmd/run-events.ts +2 -2
  106. package/src/cli/cmd/tui/component/logo.tsx +1 -1
  107. package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
  108. package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
  109. package/src/cli/cmd/tui/context/app/editor.ts +1 -1
  110. package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
  111. package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
  112. package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
  113. package/src/overlay/cost-tracker/format.ts +1 -1
  114. package/src/overlay/cost-tracker/index.ts +4 -4
  115. package/src/overlay/cost-tracker/state.ts +2 -2
  116. package/src/overlay/cost-tracker/types.ts +2 -2
  117. package/src/overlay/memory/age.ts +1 -1
  118. package/src/overlay/memory/index.ts +4 -4
  119. package/src/overlay/memory/paths.ts +2 -2
  120. package/src/overlay/memory/scan.ts +1 -1
  121. package/src/overlay/memory/types.ts +2 -2
  122. package/src/overlay/tool/bash-security.ts +3 -3
  123. package/src/overlay/util/url.ts +1 -1
  124. package/src/plugin/codex-auth.ts +1 -1
  125. package/src/provider/model-cache.ts +2 -2
  126. package/src/provider/provider-resolve.ts +3 -3
  127. package/src/provider/transform-message.ts +1 -1
  128. package/src/server/routes/game.ts +284 -0
  129. package/src/server/server.ts +2 -0
  130. package/src/session/core/compaction/compaction-helpers.ts +1 -1
  131. package/src/session/core/compaction/compaction.ts +1 -1
  132. package/src/session/core/session.ts +2 -0
  133. package/src/sessions/ingest-queue.ts +2 -2
  134. package/src/sessions/remote-ws.ts +1 -1
  135. package/src/tool/workflow/question.ts +1 -1
  136. package/src/util/abort.ts +1 -1
  137. package/src/util/bom.ts +2 -2
  138. package/src/util/color.ts +1 -1
  139. package/src/util/data-url.ts +1 -1
  140. package/src/util/defer.ts +1 -1
  141. package/src/util/error.ts +2 -2
  142. package/src/util/filesystem.ts +2 -2
  143. package/src/util/format.ts +1 -1
  144. package/src/util/iife.ts +1 -1
  145. package/src/util/local-context.ts +1 -1
  146. package/src/util/locale.ts +2 -2
  147. package/src/util/lock.ts +1 -1
  148. package/src/util/network.ts +1 -1
  149. package/src/util/signal.ts +1 -1
  150. package/src/util/token.ts +1 -1
  151. package/src/util/wildcard.ts +1 -1
  152. package/sst-env.d.ts +10 -0
  153. package/test/AGENTS.md +133 -0
  154. package/test/account/repo.test.ts +352 -0
  155. package/test/account/service.test.ts +456 -0
  156. package/test/acp/agent-interface.test.ts +51 -0
  157. package/test/acp/event-subscription.test.ts +725 -0
  158. package/test/agent/agent.test.ts +890 -0
  159. package/test/auth/auth.test.ts +86 -0
  160. package/test/bun/registry.test.ts +75 -0
  161. package/test/bus/bus-effect.test.ts +161 -0
  162. package/test/bus/bus-integration.test.ts +87 -0
  163. package/test/bus/bus.test.ts +219 -0
  164. package/test/cli/account.test.ts +26 -0
  165. package/test/cli/auto-mode.test.ts +75 -0
  166. package/test/cli/bin-saeeol.test.ts +8 -0
  167. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  168. package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
  169. package/test/cli/cmd/tui/sync.test.tsx +159 -0
  170. package/test/cli/error.test.ts +18 -0
  171. package/test/cli/github-action.test.ts +198 -0
  172. package/test/cli/github-remote.test.ts +85 -0
  173. package/test/cli/import.test.ts +97 -0
  174. package/test/cli/install-artifact.test.ts +72 -0
  175. package/test/cli/plugin-auth-picker.test.ts +120 -0
  176. package/test/cli/pr.test.ts +59 -0
  177. package/test/cli/tui/editor-context-zed.test.ts +356 -0
  178. package/test/cli/tui/editor-context.test.tsx +228 -0
  179. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  180. package/test/cli/tui/markdown.test.ts +161 -0
  181. package/test/cli/tui/plugin-add.test.ts +111 -0
  182. package/test/cli/tui/plugin-install.test.ts +87 -0
  183. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  184. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  185. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  186. package/test/cli/tui/plugin-loader.test.ts +816 -0
  187. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  188. package/test/cli/tui/revert-diff.test.ts +35 -0
  189. package/test/cli/tui/slot-replace.test.tsx +47 -0
  190. package/test/cli/tui/theme-store.test.ts +54 -0
  191. package/test/cli/tui/thread.test.ts +28 -0
  192. package/test/cli/tui/transcript.test.ts +426 -0
  193. package/test/cli/tui/usage.test.ts +60 -0
  194. package/test/cli/tui/use-event.test.tsx +175 -0
  195. package/test/config/agent-color.test.ts +67 -0
  196. package/test/config/config.test.ts +2544 -0
  197. package/test/config/fixtures/empty-frontmatter.md +4 -0
  198. package/test/config/fixtures/frontmatter.md +28 -0
  199. package/test/config/fixtures/markdown-header.md +11 -0
  200. package/test/config/fixtures/no-frontmatter.md +1 -0
  201. package/test/config/fixtures/weird-model-id.md +13 -0
  202. package/test/config/lsp.test.ts +87 -0
  203. package/test/config/markdown.test.ts +228 -0
  204. package/test/config/plugin.test.ts +0 -0
  205. package/test/config/tui.test.ts +624 -0
  206. package/test/control-plane/adapters.test.ts +71 -0
  207. package/test/control-plane/workspace.test.ts +1526 -0
  208. package/test/effect/app-runtime-logger.test.ts +98 -0
  209. package/test/effect/config-service.test.ts +65 -0
  210. package/test/effect/instance-state.test.ts +394 -0
  211. package/test/effect/run-service.test.ts +89 -0
  212. package/test/effect/runner.test.ts +523 -0
  213. package/test/fake/provider.ts +82 -0
  214. package/test/file/fsmonitor.test.ts +68 -0
  215. package/test/file/ignore.test.ts +10 -0
  216. package/test/file/index.test.ts +954 -0
  217. package/test/file/path-traversal.test.ts +205 -0
  218. package/test/file/ripgrep.test.ts +226 -0
  219. package/test/file/watcher.test.ts +249 -0
  220. package/test/filesystem/filesystem.test.ts +319 -0
  221. package/test/fixture/db.ts +11 -0
  222. package/test/fixture/fixture.test.ts +26 -0
  223. package/test/fixture/fixture.ts +175 -0
  224. package/test/fixture/flock-worker.ts +72 -0
  225. package/test/fixture/log-init-worker.ts +62 -0
  226. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  227. package/test/fixture/plug-worker.ts +93 -0
  228. package/test/fixture/plugin-meta-worker.ts +19 -0
  229. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  230. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  231. package/test/fixture/skills/index.json +6 -0
  232. package/test/fixture/tui-plugin.ts +323 -0
  233. package/test/fixture/tui-runtime.ts +31 -0
  234. package/test/format/format.test.ts +272 -0
  235. package/test/git/git.test.ts +128 -0
  236. package/test/ide/ide.test.ts +82 -0
  237. package/test/installation/installation.test.ts +168 -0
  238. package/test/keybind.test.ts +421 -0
  239. package/test/lib/effect.ts +53 -0
  240. package/test/lib/filesystem.ts +10 -0
  241. package/test/lib/llm-server.ts +778 -0
  242. package/test/lib/websocket.ts +46 -0
  243. package/test/lsp/client.test.ts +482 -0
  244. package/test/lsp/index.test.ts +160 -0
  245. package/test/lsp/launch.test.ts +22 -0
  246. package/test/lsp/lifecycle.test.ts +184 -0
  247. package/test/ltm/ltm.test.ts +230 -0
  248. package/test/mcp/headers.test.ts +178 -0
  249. package/test/mcp/lifecycle.test.ts +787 -0
  250. package/test/mcp/oauth-auto-connect.test.ts +311 -0
  251. package/test/mcp/oauth-browser.test.ts +276 -0
  252. package/test/mcp/oauth-callback.test.ts +34 -0
  253. package/test/memory/abort-leak-webfetch.ts +49 -0
  254. package/test/memory/abort-leak.test.ts +128 -0
  255. package/test/patch/patch.test.ts +348 -0
  256. package/test/permission/arity.test.ts +33 -0
  257. package/test/permission/next.test.ts +1227 -0
  258. package/test/permission/next.toConfig.test.ts +110 -0
  259. package/test/permission-task.test.ts +326 -0
  260. package/test/plugin/auth-override.test.ts +79 -0
  261. package/test/plugin/cloudflare.test.ts +68 -0
  262. package/test/plugin/codex.test.ts +123 -0
  263. package/test/plugin/github-copilot-models.test.ts +261 -0
  264. package/test/plugin/install-concurrency.test.ts +140 -0
  265. package/test/plugin/install.test.ts +570 -0
  266. package/test/plugin/loader-shared.test.ts +1169 -0
  267. package/test/plugin/meta.test.ts +137 -0
  268. package/test/plugin/plugin-contract.test.ts +291 -0
  269. package/test/plugin/shared.test.ts +88 -0
  270. package/test/plugin/trigger.test.ts +102 -0
  271. package/test/plugin/workspace-adapter.test.ts +109 -0
  272. package/test/preload.ts +77 -0
  273. package/test/project/instance.test.ts +276 -0
  274. package/test/project/migrate-global.test.ts +152 -0
  275. package/test/project/project.test.ts +600 -0
  276. package/test/project/vcs.test.ts +286 -0
  277. package/test/project/worktree-remove.test.ts +126 -0
  278. package/test/project/worktree.test.ts +223 -0
  279. package/test/provider/amazon-bedrock.test.ts +462 -0
  280. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  281. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  282. package/test/provider/gitlab-duo.test.ts +413 -0
  283. package/test/provider/local.test.ts +208 -0
  284. package/test/provider/models.test.ts +261 -0
  285. package/test/provider/provider-category.test.ts +190 -0
  286. package/test/provider/provider.test.ts +2758 -0
  287. package/test/provider/transform.test.ts +3681 -0
  288. package/test/pty/pty-output-isolation.test.ts +147 -0
  289. package/test/pty/pty-session.test.ts +102 -0
  290. package/test/pty/pty-shell.test.ts +104 -0
  291. package/test/question/question.test.ts +490 -0
  292. package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
  293. package/test/saeeol/agent-manager-tool.test.ts +71 -0
  294. package/test/saeeol/agent-permission-overrides.test.ts +75 -0
  295. package/test/saeeol/agent-skill-permissions.test.ts +37 -0
  296. package/test/saeeol/ask-agent-permissions.test.ts +303 -0
  297. package/test/saeeol/bash-hierarchy.test.ts +64 -0
  298. package/test/saeeol/bash-permission-metadata.test.ts +66 -0
  299. package/test/saeeol/bash-security-extended.test.ts +243 -0
  300. package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
  301. package/test/saeeol/boxes-integration.test.ts +415 -0
  302. package/test/saeeol/builtin-skills.test.ts +75 -0
  303. package/test/saeeol/cleanup.ts +28 -0
  304. package/test/saeeol/cli/dev-setup.test.ts +74 -0
  305. package/test/saeeol/cli/roll-call.test.ts +161 -0
  306. package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
  307. package/test/saeeol/codex-auth-refresh.test.ts +124 -0
  308. package/test/saeeol/commit-message/generate.test.ts +188 -0
  309. package/test/saeeol/commit-message/git-context.test.ts +303 -0
  310. package/test/saeeol/commit-message-windows.test.ts +38 -0
  311. package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
  312. package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
  313. package/test/saeeol/compaction-skip-guard.test.ts +224 -0
  314. package/test/saeeol/compaction-smart-select.test.ts +100 -0
  315. package/test/saeeol/config/config.test.ts +166 -0
  316. package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
  317. package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
  318. package/test/saeeol/config-gitignore.test.ts +70 -0
  319. package/test/saeeol/config-injector.test.ts +305 -0
  320. package/test/saeeol/config-resilience.test.ts +234 -0
  321. package/test/saeeol/config-validation.test.ts +183 -0
  322. package/test/saeeol/cost-propagation.test.ts +94 -0
  323. package/test/saeeol/cost-tracker-extended.test.ts +141 -0
  324. package/test/saeeol/cost-tracker.test.ts +64 -0
  325. package/test/saeeol/custom-provider-delete.test.ts +149 -0
  326. package/test/saeeol/diff-full.test.ts +226 -0
  327. package/test/saeeol/edit-permission-filediff.test.ts +223 -0
  328. package/test/saeeol/encoding.test.ts +364 -0
  329. package/test/saeeol/enhance-prompt.test.ts +61 -0
  330. package/test/saeeol/ensure-plan-dir.test.ts +32 -0
  331. package/test/saeeol/errors.test.ts +144 -0
  332. package/test/saeeol/external-directory-boundary.test.ts +96 -0
  333. package/test/saeeol/gateway-headers.test.ts +88 -0
  334. package/test/saeeol/help.test.ts +191 -0
  335. package/test/saeeol/ignore-migrator.test.ts +308 -0
  336. package/test/saeeol/indexing-auth.test.ts +45 -0
  337. package/test/saeeol/indexing-feature.test.ts +44 -0
  338. package/test/saeeol/indexing-label.test.ts +70 -0
  339. package/test/saeeol/indexing-startup.test.ts +381 -0
  340. package/test/saeeol/indexing-worktree.test.ts +73 -0
  341. package/test/saeeol/instruction.test.ts +136 -0
  342. package/test/saeeol/lancedb-runtime.test.ts +116 -0
  343. package/test/saeeol/loader-auth.test.ts +168 -0
  344. package/test/saeeol/local-model.test.ts +621 -0
  345. package/test/saeeol/logo.test.ts +31 -0
  346. package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
  347. package/test/saeeol/mcp-branding.test.ts +33 -0
  348. package/test/saeeol/mcp-docker-rm.test.ts +32 -0
  349. package/test/saeeol/mcp-migrator.test.ts +736 -0
  350. package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
  351. package/test/saeeol/memory-io.test.ts +198 -0
  352. package/test/saeeol/memory-paths.test.ts +87 -0
  353. package/test/saeeol/memory-security.test.ts +166 -0
  354. package/test/saeeol/model-cache-org.test.ts +164 -0
  355. package/test/saeeol/model-info-panel-utils.test.ts +52 -0
  356. package/test/saeeol/model-info-panel.types.test.ts +7 -0
  357. package/test/saeeol/models-401-fallback.test.ts +52 -0
  358. package/test/saeeol/modes-migrator.test.ts +320 -0
  359. package/test/saeeol/nvidia-headers.test.ts +74 -0
  360. package/test/saeeol/patch-jsonc.test.ts +73 -0
  361. package/test/saeeol/patch.test.ts +172 -0
  362. package/test/saeeol/paths.test.ts +265 -0
  363. package/test/saeeol/permission/config-paths.test.ts +174 -0
  364. package/test/saeeol/permission/env-read.test.ts +149 -0
  365. package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
  366. package/test/saeeol/permission/next.always-rules.test.ts +882 -0
  367. package/test/saeeol/permission/next.reply-http.test.ts +205 -0
  368. package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
  369. package/test/saeeol/plan-exit-detection.test.ts +494 -0
  370. package/test/saeeol/plan-followup.test.ts +1376 -0
  371. package/test/saeeol/project-config-update.test.ts +120 -0
  372. package/test/saeeol/project-id.test.ts +455 -0
  373. package/test/saeeol/provider-cost.test.ts +171 -0
  374. package/test/saeeol/provider-list-failed-state.test.ts +100 -0
  375. package/test/saeeol/question-dismiss-all.test.ts +174 -0
  376. package/test/saeeol/read-directory.test.ts +116 -0
  377. package/test/saeeol/rules-migrator.test.ts +257 -0
  378. package/test/saeeol/run-auto.test.ts +176 -0
  379. package/test/saeeol/run-network.test.ts +224 -0
  380. package/test/saeeol/semantic-search.test.ts +186 -0
  381. package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
  382. package/test/saeeol/session/instruction-substitution.test.ts +72 -0
  383. package/test/saeeol/session/platform-attribution.test.ts +118 -0
  384. package/test/saeeol/session/session.test.ts +105 -0
  385. package/test/saeeol/session-compaction-cap.test.ts +399 -0
  386. package/test/saeeol/session-compaction-chunks.test.ts +501 -0
  387. package/test/saeeol/session-compaction-safety.test.ts +481 -0
  388. package/test/saeeol/session-fork-remap.test.ts +251 -0
  389. package/test/saeeol/session-import-service.test.ts +114 -0
  390. package/test/saeeol/session-list.test.ts +47 -0
  391. package/test/saeeol/session-message-metadata.test.ts +128 -0
  392. package/test/saeeol/session-overflow.test.ts +78 -0
  393. package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
  394. package/test/saeeol/session-processor-network-offline.test.ts +204 -0
  395. package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
  396. package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
  397. package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
  398. package/test/saeeol/session-prompt-queue.test.ts +815 -0
  399. package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
  400. package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
  401. package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
  402. package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
  403. package/test/saeeol/sessions/remote-ws.test.ts +367 -0
  404. package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
  405. package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
  406. package/test/saeeol/snapshot-cache.test.ts +84 -0
  407. package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
  408. package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
  409. package/test/saeeol/stats-subagent-cost.test.ts +123 -0
  410. package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
  411. package/test/saeeol/suggestion/suggestion.test.ts +145 -0
  412. package/test/saeeol/suggestion/tool.test.ts +298 -0
  413. package/test/saeeol/summary-file-diff.test.ts +28 -0
  414. package/test/saeeol/system-prompt.test.ts +142 -0
  415. package/test/saeeol/task-nesting.test.ts +193 -0
  416. package/test/saeeol/telemetry/feedback.test.ts +8 -0
  417. package/test/saeeol/todo-view.test.ts +57 -0
  418. package/test/saeeol/tool-encoding.test.ts +455 -0
  419. package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
  420. package/test/saeeol/tool-registry-indexing.test.ts +236 -0
  421. package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
  422. package/test/saeeol/tool-task-model.test.ts +352 -0
  423. package/test/saeeol/transform-opus-4.7.test.ts +89 -0
  424. package/test/saeeol/tui-diff.test.ts +91 -0
  425. package/test/saeeol/tui-sync.test.ts +80 -0
  426. package/test/saeeol/util/url.test.ts +141 -0
  427. package/test/saeeol/workflows-migrator.test.ts +261 -0
  428. package/test/saeeol/worktree-diff-summary.test.ts +64 -0
  429. package/test/saeeol/worktree-diff.test.ts +223 -0
  430. package/test/saeeol/worktree-remove-lock.test.ts +82 -0
  431. package/test/server/AGENTS.md +15 -0
  432. package/test/server/contract.test.ts +357 -0
  433. package/test/server/experimental-session-list.test.ts +157 -0
  434. package/test/server/global-session-list.test.ts +155 -0
  435. package/test/server/httpapi-authorization.test.ts +103 -0
  436. package/test/server/httpapi-bridge.test.ts +440 -0
  437. package/test/server/httpapi-config.test.ts +67 -0
  438. package/test/server/httpapi-cors.test.ts +89 -0
  439. package/test/server/httpapi-event.test.ts +57 -0
  440. package/test/server/httpapi-experimental.test.ts +219 -0
  441. package/test/server/httpapi-file.test.ts +79 -0
  442. package/test/server/httpapi-instance-context.test.ts +237 -0
  443. package/test/server/httpapi-instance.legacy.test.ts +140 -0
  444. package/test/server/httpapi-instance.test.ts +83 -0
  445. package/test/server/httpapi-json-parity.test.ts +263 -0
  446. package/test/server/httpapi-mcp-oauth.test.ts +76 -0
  447. package/test/server/httpapi-mcp.test.ts +189 -0
  448. package/test/server/httpapi-provider.test.ts +153 -0
  449. package/test/server/httpapi-pty-websocket.test.ts +16 -0
  450. package/test/server/httpapi-pty.test.ts +175 -0
  451. package/test/server/httpapi-raw-route-auth.test.ts +89 -0
  452. package/test/server/httpapi-sdk.test.ts +681 -0
  453. package/test/server/httpapi-session.test.ts +464 -0
  454. package/test/server/httpapi-sync.test.ts +130 -0
  455. package/test/server/httpapi-tui.test.ts +121 -0
  456. package/test/server/httpapi-workspace-routing.test.ts +471 -0
  457. package/test/server/httpapi-workspace.test.ts +427 -0
  458. package/test/server/lib/conformance.ts +88 -0
  459. package/test/server/lib/stateful.ts +112 -0
  460. package/test/server/project-init-git.test.ts +113 -0
  461. package/test/server/proxy-util.test.ts +113 -0
  462. package/test/server/session-actions.test.ts +49 -0
  463. package/test/server/session-list.test.ts +238 -0
  464. package/test/server/session-messages.test.ts +167 -0
  465. package/test/server/session-select.test.ts +100 -0
  466. package/test/server/trace-attributes.test.ts +76 -0
  467. package/test/server/workspace-proxy.test.ts +165 -0
  468. package/test/server/workspace-routing.test.ts +85 -0
  469. package/test/session/compaction.test.ts +2420 -0
  470. package/test/session/instruction.test.ts +247 -0
  471. package/test/session/llm.test.ts +1273 -0
  472. package/test/session/message-v2.test.ts +1291 -0
  473. package/test/session/messages-pagination.test.ts +1173 -0
  474. package/test/session/network.test.ts +249 -0
  475. package/test/session/processor-effect.test.ts +847 -0
  476. package/test/session/prompt.test.ts +2131 -0
  477. package/test/session/retry.test.ts +340 -0
  478. package/test/session/revert-compact.test.ts +639 -0
  479. package/test/session/schema-decoding.test.ts +311 -0
  480. package/test/session/session-entry-stepper.test.ts +917 -0
  481. package/test/session/session-schema.test.ts +76 -0
  482. package/test/session/snapshot-tool-race.test.ts +257 -0
  483. package/test/session/structured-output-integration.test.ts +265 -0
  484. package/test/session/structured-output.test.ts +381 -0
  485. package/test/session/system.test.ts +73 -0
  486. package/test/share/share-next.test.ts +333 -0
  487. package/test/shell/shell.test.ts +99 -0
  488. package/test/skill/discovery.test.ts +116 -0
  489. package/test/skill/skill.test.ts +393 -0
  490. package/test/snapshot/snapshot.test.ts +1531 -0
  491. package/test/storage/db.test.ts +23 -0
  492. package/test/storage/json-migration.test.ts +832 -0
  493. package/test/storage/storage.test.ts +293 -0
  494. package/test/suggestion/suggestion.test.ts +1 -0
  495. package/test/sync/index.test.ts +256 -0
  496. package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
  497. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  498. package/test/tool/apply_patch.test.ts +614 -0
  499. package/test/tool/bash.test.ts +1225 -0
  500. package/test/tool/diagnostics-filter.test.ts +55 -0
  501. package/test/tool/edit.test.ts +754 -0
  502. package/test/tool/external-directory.test.ts +169 -0
  503. package/test/tool/fixtures/large-image.png +0 -0
  504. package/test/tool/fixtures/models-api.json +65179 -0
  505. package/test/tool/glob.test.ts +107 -0
  506. package/test/tool/grep.test.ts +114 -0
  507. package/test/tool/lsp.test.ts +187 -0
  508. package/test/tool/parameters.test.ts +243 -0
  509. package/test/tool/question.test.ts +129 -0
  510. package/test/tool/read.test.ts +500 -0
  511. package/test/tool/recall.test.ts +151 -0
  512. package/test/tool/registry.test.ts +203 -0
  513. package/test/tool/skill.test.ts +135 -0
  514. package/test/tool/suggest.test.ts +1 -0
  515. package/test/tool/task.test.ts +612 -0
  516. package/test/tool/tool-define.test.ts +99 -0
  517. package/test/tool/truncation.test.ts +260 -0
  518. package/test/tool/webfetch.test.ts +103 -0
  519. package/test/tool/write.test.ts +291 -0
  520. package/test/util/data-url.test.ts +14 -0
  521. package/test/util/effect-zod.test.ts +754 -0
  522. package/test/util/error.test.ts +38 -0
  523. package/test/util/filesystem.test.ts +656 -0
  524. package/test/util/format.test.ts +59 -0
  525. package/test/util/glob.test.ts +164 -0
  526. package/test/util/iife.test.ts +36 -0
  527. package/test/util/lazy.test.ts +50 -0
  528. package/test/util/lock.test.ts +72 -0
  529. package/test/util/log.test.ts +86 -0
  530. package/test/util/module.test.ts +59 -0
  531. package/test/util/process.test.ts +128 -0
  532. package/test/util/timeout.test.ts +21 -0
  533. package/test/util/which.test.ts +100 -0
  534. package/test/util/wildcard.test.ts +90 -0
  535. package/test/workspace/workspace-restore.test.ts +296 -0
  536. package/src/provider/models-snapshot.d.ts +0 -2
  537. package/src/provider/models-snapshot.js +0 -3
@@ -0,0 +1,787 @@
1
+ import { test, expect, mock, beforeEach } from "bun:test"
2
+ import { InstanceStore } from "../../src/project/instance-store"
3
+ import { Effect } from "effect"
4
+ import type { MCP as MCPNS } from "../../src/mcp/index"
5
+
6
+ // --- Mock infrastructure ---
7
+
8
+ // Per-client state for controlling mock behavior
9
+ interface MockClientState {
10
+ tools: Array<{ name: string; description?: string; inputSchema: object }>
11
+ listToolsCalls: number
12
+ listToolsShouldFail: boolean
13
+ listToolsError: string
14
+ listPromptsShouldFail: boolean
15
+ listResourcesShouldFail: boolean
16
+ prompts: Array<{ name: string; description?: string }>
17
+ resources: Array<{ name: string; uri: string; description?: string }>
18
+ closed: boolean
19
+ notificationHandlers: Map<unknown, (...args: any[]) => any>
20
+ }
21
+
22
+ const clientStates = new Map<string, MockClientState>()
23
+ let lastCreatedClientName: string | undefined
24
+ let connectShouldFail = false
25
+ let connectShouldHang = false
26
+ let connectError = "Mock transport cannot connect"
27
+ // Tracks how many Client instances were created (detects leaks)
28
+ let clientCreateCount = 0
29
+ // Tracks how many times transport.close() is called across all mock transports
30
+ let transportCloseCount = 0
31
+
32
+ function getOrCreateClientState(name?: string): MockClientState {
33
+ const key = name ?? "default"
34
+ let state = clientStates.get(key)
35
+ if (!state) {
36
+ state = {
37
+ tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }],
38
+ listToolsCalls: 0,
39
+ listToolsShouldFail: false,
40
+ listToolsError: "listTools failed",
41
+ listPromptsShouldFail: false,
42
+ listResourcesShouldFail: false,
43
+ prompts: [],
44
+ resources: [],
45
+ closed: false,
46
+ notificationHandlers: new Map(),
47
+ }
48
+ clientStates.set(key, state)
49
+ }
50
+ return state
51
+ }
52
+
53
+ // Mock transport that succeeds or fails based on connectShouldFail / connectShouldHang
54
+ class MockStdioTransport {
55
+ stderr: null = null
56
+ pid = 12345
57
+ // oxlint-disable-next-line no-useless-constructor
58
+ constructor(_opts: any) {}
59
+ async start() {
60
+ if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
61
+ if (connectShouldFail) throw new Error(connectError)
62
+ }
63
+ async close() {
64
+ transportCloseCount++
65
+ }
66
+ }
67
+
68
+ class MockStreamableHTTP {
69
+ // oxlint-disable-next-line no-useless-constructor
70
+ constructor(_url: URL, _opts?: any) {}
71
+ async start() {
72
+ if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
73
+ if (connectShouldFail) throw new Error(connectError)
74
+ }
75
+ async close() {
76
+ transportCloseCount++
77
+ }
78
+ async finishAuth() {}
79
+ }
80
+
81
+ class MockSSE {
82
+ // oxlint-disable-next-line no-useless-constructor
83
+ constructor(_url: URL, _opts?: any) {}
84
+ async start() {
85
+ if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
86
+ if (connectShouldFail) throw new Error(connectError)
87
+ }
88
+ async close() {
89
+ transportCloseCount++
90
+ }
91
+ }
92
+
93
+ void mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({
94
+ StdioClientTransport: MockStdioTransport,
95
+ }))
96
+
97
+ void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
98
+ StreamableHTTPClientTransport: MockStreamableHTTP,
99
+ }))
100
+
101
+ void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
102
+ SSEClientTransport: MockSSE,
103
+ }))
104
+
105
+ void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
106
+ UnauthorizedError: class extends Error {
107
+ constructor() {
108
+ super("Unauthorized")
109
+ }
110
+ },
111
+ }))
112
+
113
+ // Mock Client that delegates to per-name MockClientState
114
+ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
115
+ Client: class MockClient {
116
+ _state!: MockClientState
117
+ transport: any
118
+
119
+ constructor(_opts: any) {
120
+ clientCreateCount++
121
+ }
122
+
123
+ async connect(transport: { start: () => Promise<void> }) {
124
+ this.transport = transport
125
+ await transport.start()
126
+ // After successful connect, bind to the last-created client name
127
+ this._state = getOrCreateClientState(lastCreatedClientName)
128
+ }
129
+
130
+ setNotificationHandler(schema: unknown, handler: (...args: any[]) => any) {
131
+ this._state?.notificationHandlers.set(schema, handler)
132
+ }
133
+
134
+ async listTools() {
135
+ if (this._state) this._state.listToolsCalls++
136
+ if (this._state?.listToolsShouldFail) {
137
+ throw new Error(this._state.listToolsError)
138
+ }
139
+ return { tools: this._state?.tools ?? [] }
140
+ }
141
+
142
+ async listPrompts() {
143
+ if (this._state?.listPromptsShouldFail) {
144
+ throw new Error("listPrompts failed")
145
+ }
146
+ return { prompts: this._state?.prompts ?? [] }
147
+ }
148
+
149
+ async listResources() {
150
+ if (this._state?.listResourcesShouldFail) {
151
+ throw new Error("listResources failed")
152
+ }
153
+ return { resources: this._state?.resources ?? [] }
154
+ }
155
+
156
+ async close() {
157
+ if (this._state) this._state.closed = true
158
+ }
159
+ },
160
+ }))
161
+
162
+ beforeEach(() => {
163
+ clientStates.clear()
164
+ lastCreatedClientName = undefined
165
+ connectShouldFail = false
166
+ connectShouldHang = false
167
+ connectError = "Mock transport cannot connect"
168
+ clientCreateCount = 0
169
+ transportCloseCount = 0
170
+ })
171
+
172
+ // Import after mocks
173
+ const { MCP } = await import("../../src/mcp/index")
174
+ const { Instance } = await import("../../src/project/instance")
175
+ const { tmpdir } = await import("../fixture/fixture")
176
+
177
+ // --- Helper ---
178
+
179
+ function withInstance(
180
+ config: Record<string, unknown>,
181
+ fn: (mcp: MCPNS.Interface) => Effect.Effect<void, unknown, never>,
182
+ ) {
183
+ return async () => {
184
+ await using tmp = await tmpdir({
185
+ init: async (dir) => {
186
+ await Bun.write(
187
+ `${dir}/saeeol.json`,
188
+ JSON.stringify({
189
+ $schema: "https://saeeol.ai/config.json",
190
+ mcp: config,
191
+ }),
192
+ )
193
+ },
194
+ })
195
+
196
+ await Instance.provide({
197
+ directory: tmp.path,
198
+ fn: async () => {
199
+ await Effect.runPromise(MCP.Service.use(fn).pipe(Effect.provide(MCP.defaultLayer)))
200
+ // dispose instance to clean up state between tests
201
+ await InstanceStore.disposeInstance(Instance.current)
202
+ },
203
+ })
204
+ }
205
+ }
206
+
207
+ // ========================================================================
208
+ // Test: tools() are cached after connect
209
+ // ========================================================================
210
+
211
+ test(
212
+ "tools() reuses cached tool definitions after connect",
213
+ withInstance({}, (mcp) =>
214
+ Effect.gen(function* () {
215
+ lastCreatedClientName = "my-server"
216
+ const serverState = getOrCreateClientState("my-server")
217
+ serverState.tools = [
218
+ { name: "do_thing", description: "does a thing", inputSchema: { type: "object", properties: {} } },
219
+ ]
220
+
221
+ // First: add the server successfully
222
+ const addResult = yield* mcp.add("my-server", {
223
+ type: "local",
224
+ command: ["echo", "test"],
225
+ })
226
+ expect((addResult.status as any)["my-server"]?.status ?? (addResult.status as any).status).toBe("connected")
227
+
228
+ expect(serverState.listToolsCalls).toBe(1)
229
+
230
+ const toolsA = yield* mcp.tools()
231
+ const toolsB = yield* mcp.tools()
232
+ expect(Object.keys(toolsA).length).toBeGreaterThan(0)
233
+ expect(Object.keys(toolsB).length).toBeGreaterThan(0)
234
+ expect(serverState.listToolsCalls).toBe(1)
235
+ }),
236
+ ),
237
+ )
238
+
239
+ // ========================================================================
240
+ // Test: tool change notifications refresh the cache
241
+ // ========================================================================
242
+
243
+ test(
244
+ "tool change notifications refresh cached tool definitions",
245
+ withInstance({}, (mcp) =>
246
+ Effect.gen(function* () {
247
+ lastCreatedClientName = "status-server"
248
+ const serverState = getOrCreateClientState("status-server")
249
+
250
+ yield* mcp.add("status-server", {
251
+ type: "local",
252
+ command: ["echo", "test"],
253
+ })
254
+
255
+ const before = yield* mcp.tools()
256
+ expect(Object.keys(before).some((key) => key.includes("test_tool"))).toBe(true)
257
+ expect(serverState.listToolsCalls).toBe(1)
258
+
259
+ serverState.tools = [{ name: "next_tool", description: "next", inputSchema: { type: "object", properties: {} } }]
260
+
261
+ const handler = Array.from(serverState.notificationHandlers.values())[0]
262
+ expect(handler).toBeDefined()
263
+ yield* Effect.promise(() => handler?.())
264
+
265
+ const after = yield* mcp.tools()
266
+ expect(Object.keys(after).some((key) => key.includes("next_tool"))).toBe(true)
267
+ expect(Object.keys(after).some((key) => key.includes("test_tool"))).toBe(false)
268
+ expect(serverState.listToolsCalls).toBe(2)
269
+ }),
270
+ ),
271
+ )
272
+
273
+ // ========================================================================
274
+ // Test: connect() / disconnect() lifecycle
275
+ // ========================================================================
276
+
277
+ test(
278
+ "disconnect sets status to disabled and removes client",
279
+ withInstance(
280
+ {
281
+ "disc-server": {
282
+ type: "local",
283
+ command: ["echo", "test"],
284
+ },
285
+ },
286
+ (mcp) =>
287
+ Effect.gen(function* () {
288
+ lastCreatedClientName = "disc-server"
289
+ getOrCreateClientState("disc-server")
290
+
291
+ yield* mcp.add("disc-server", {
292
+ type: "local",
293
+ command: ["echo", "test"],
294
+ })
295
+
296
+ const statusBefore = yield* mcp.status()
297
+ expect(statusBefore["disc-server"]?.status).toBe("connected")
298
+
299
+ yield* mcp.disconnect("disc-server")
300
+
301
+ const statusAfter = yield* mcp.status()
302
+ expect(statusAfter["disc-server"]?.status).toBe("disabled")
303
+
304
+ // Tools should be empty after disconnect
305
+ const tools = yield* mcp.tools()
306
+ const serverTools = Object.keys(tools).filter((k) => k.startsWith("disc-server"))
307
+ expect(serverTools.length).toBe(0)
308
+ }),
309
+ ),
310
+ )
311
+
312
+ test(
313
+ "connect() after disconnect() re-establishes the server",
314
+ withInstance(
315
+ {
316
+ "reconn-server": {
317
+ type: "local",
318
+ command: ["echo", "test"],
319
+ },
320
+ },
321
+ (mcp) =>
322
+ Effect.gen(function* () {
323
+ lastCreatedClientName = "reconn-server"
324
+ const serverState = getOrCreateClientState("reconn-server")
325
+ serverState.tools = [
326
+ { name: "my_tool", description: "a tool", inputSchema: { type: "object", properties: {} } },
327
+ ]
328
+
329
+ yield* mcp.add("reconn-server", {
330
+ type: "local",
331
+ command: ["echo", "test"],
332
+ })
333
+
334
+ yield* mcp.disconnect("reconn-server")
335
+ expect((yield* mcp.status())["reconn-server"]?.status).toBe("disabled")
336
+
337
+ // Reconnect
338
+ yield* mcp.connect("reconn-server")
339
+ expect((yield* mcp.status())["reconn-server"]?.status).toBe("connected")
340
+
341
+ const tools = yield* mcp.tools()
342
+ expect(Object.keys(tools).some((k) => k.includes("my_tool"))).toBe(true)
343
+ }),
344
+ ),
345
+ )
346
+
347
+ // ========================================================================
348
+ // Test: add() closes existing client before replacing
349
+ // ========================================================================
350
+
351
+ test(
352
+ "add() closes the old client when replacing a server",
353
+ // Don't put the server in config — add it dynamically so we control
354
+ // exactly which client instance is "first" vs "second".
355
+ withInstance({}, (mcp) =>
356
+ Effect.gen(function* () {
357
+ lastCreatedClientName = "replace-server"
358
+ const firstState = getOrCreateClientState("replace-server")
359
+
360
+ yield* mcp.add("replace-server", {
361
+ type: "local",
362
+ command: ["echo", "test"],
363
+ })
364
+
365
+ expect(firstState.closed).toBe(false)
366
+
367
+ // Create new state for second client
368
+ clientStates.delete("replace-server")
369
+ const secondState = getOrCreateClientState("replace-server")
370
+
371
+ // Re-add should close the first client
372
+ yield* mcp.add("replace-server", {
373
+ type: "local",
374
+ command: ["echo", "test"],
375
+ })
376
+
377
+ expect(firstState.closed).toBe(true)
378
+ expect(secondState.closed).toBe(false)
379
+ }),
380
+ ),
381
+ )
382
+
383
+ // ========================================================================
384
+ // Test: state init with mixed success/failure
385
+ // ========================================================================
386
+
387
+ test(
388
+ "init connects available servers even when one fails",
389
+ withInstance(
390
+ {
391
+ "good-server": {
392
+ type: "local",
393
+ command: ["echo", "good"],
394
+ },
395
+ "bad-server": {
396
+ type: "local",
397
+ command: ["echo", "bad"],
398
+ },
399
+ },
400
+ (mcp) =>
401
+ Effect.gen(function* () {
402
+ // Set up good server
403
+ const goodState = getOrCreateClientState("good-server")
404
+ goodState.tools = [{ name: "good_tool", description: "works", inputSchema: { type: "object", properties: {} } }]
405
+
406
+ // Set up bad server - will fail on listTools during create()
407
+ const badState = getOrCreateClientState("bad-server")
408
+ badState.listToolsShouldFail = true
409
+
410
+ // Add good server first
411
+ lastCreatedClientName = "good-server"
412
+ yield* mcp.add("good-server", {
413
+ type: "local",
414
+ command: ["echo", "good"],
415
+ })
416
+
417
+ // Add bad server - should fail but not affect good server
418
+ lastCreatedClientName = "bad-server"
419
+ yield* mcp.add("bad-server", {
420
+ type: "local",
421
+ command: ["echo", "bad"],
422
+ })
423
+
424
+ const status = yield* mcp.status()
425
+ expect(status["good-server"]?.status).toBe("connected")
426
+ expect(status["bad-server"]?.status).toBe("failed")
427
+
428
+ // Good server's tools should still be available
429
+ const tools = yield* mcp.tools()
430
+ expect(Object.keys(tools).some((k) => k.includes("good_tool"))).toBe(true)
431
+ }),
432
+ ),
433
+ )
434
+
435
+ // ========================================================================
436
+ // Test: disabled server via config
437
+ // ========================================================================
438
+
439
+ test(
440
+ "disabled server is marked as disabled without attempting connection",
441
+ withInstance(
442
+ {
443
+ "disabled-server": {
444
+ type: "local",
445
+ command: ["echo", "test"],
446
+ enabled: false,
447
+ },
448
+ },
449
+ (mcp) =>
450
+ Effect.gen(function* () {
451
+ const countBefore = clientCreateCount
452
+
453
+ yield* mcp.add("disabled-server", {
454
+ type: "local",
455
+ command: ["echo", "test"],
456
+ enabled: false,
457
+ } as any)
458
+
459
+ // No client should have been created
460
+ expect(clientCreateCount).toBe(countBefore)
461
+
462
+ const status = yield* mcp.status()
463
+ expect(status["disabled-server"]?.status).toBe("disabled")
464
+ }),
465
+ ),
466
+ )
467
+
468
+ // ========================================================================
469
+ // Test: prompts() and resources()
470
+ // ========================================================================
471
+
472
+ test(
473
+ "prompts() returns prompts from connected servers",
474
+ withInstance(
475
+ {
476
+ "prompt-server": {
477
+ type: "local",
478
+ command: ["echo", "test"],
479
+ },
480
+ },
481
+ (mcp) =>
482
+ Effect.gen(function* () {
483
+ lastCreatedClientName = "prompt-server"
484
+ const serverState = getOrCreateClientState("prompt-server")
485
+ serverState.prompts = [{ name: "my-prompt", description: "A test prompt" }]
486
+
487
+ yield* mcp.add("prompt-server", {
488
+ type: "local",
489
+ command: ["echo", "test"],
490
+ })
491
+
492
+ const prompts = yield* mcp.prompts()
493
+ expect(Object.keys(prompts).length).toBe(1)
494
+ const key = Object.keys(prompts)[0]
495
+ expect(key).toContain("prompt-server")
496
+ expect(key).toContain("my-prompt")
497
+ }),
498
+ ),
499
+ )
500
+
501
+ test(
502
+ "resources() returns resources from connected servers",
503
+ withInstance(
504
+ {
505
+ "resource-server": {
506
+ type: "local",
507
+ command: ["echo", "test"],
508
+ },
509
+ },
510
+ (mcp) =>
511
+ Effect.gen(function* () {
512
+ lastCreatedClientName = "resource-server"
513
+ const serverState = getOrCreateClientState("resource-server")
514
+ serverState.resources = [{ name: "my-resource", uri: "file:///test.txt", description: "A test resource" }]
515
+
516
+ yield* mcp.add("resource-server", {
517
+ type: "local",
518
+ command: ["echo", "test"],
519
+ })
520
+
521
+ const resources = yield* mcp.resources()
522
+ expect(Object.keys(resources).length).toBe(1)
523
+ const key = Object.keys(resources)[0]
524
+ expect(key).toContain("resource-server")
525
+ expect(key).toContain("my-resource")
526
+ }),
527
+ ),
528
+ )
529
+
530
+ test(
531
+ "prompts() skips disconnected servers",
532
+ withInstance(
533
+ {
534
+ "prompt-disc-server": {
535
+ type: "local",
536
+ command: ["echo", "test"],
537
+ },
538
+ },
539
+ (mcp) =>
540
+ Effect.gen(function* () {
541
+ lastCreatedClientName = "prompt-disc-server"
542
+ const serverState = getOrCreateClientState("prompt-disc-server")
543
+ serverState.prompts = [{ name: "hidden-prompt", description: "Should not appear" }]
544
+
545
+ yield* mcp.add("prompt-disc-server", {
546
+ type: "local",
547
+ command: ["echo", "test"],
548
+ })
549
+
550
+ yield* mcp.disconnect("prompt-disc-server")
551
+
552
+ const prompts = yield* mcp.prompts()
553
+ expect(Object.keys(prompts).length).toBe(0)
554
+ }),
555
+ ),
556
+ )
557
+
558
+ // ========================================================================
559
+ // Test: connect() on nonexistent server
560
+ // ========================================================================
561
+
562
+ test(
563
+ "connect() on nonexistent server does not throw",
564
+ withInstance({}, (mcp) =>
565
+ Effect.gen(function* () {
566
+ // Should not throw
567
+ yield* mcp.connect("nonexistent")
568
+ const status = yield* mcp.status()
569
+ expect(status["nonexistent"]).toBeUndefined()
570
+ }),
571
+ ),
572
+ )
573
+
574
+ // ========================================================================
575
+ // Test: disconnect() on nonexistent server
576
+ // ========================================================================
577
+
578
+ test(
579
+ "disconnect() on nonexistent server does not throw",
580
+ withInstance({}, (mcp) =>
581
+ Effect.gen(function* () {
582
+ yield* mcp.disconnect("nonexistent")
583
+ // Should complete without error
584
+ }),
585
+ ),
586
+ )
587
+
588
+ // ========================================================================
589
+ // Test: tools() with no MCP servers configured
590
+ // ========================================================================
591
+
592
+ test(
593
+ "tools() returns empty when no MCP servers are configured",
594
+ withInstance({}, (mcp) =>
595
+ Effect.gen(function* () {
596
+ const tools = yield* mcp.tools()
597
+ expect(Object.keys(tools).length).toBe(0)
598
+ }),
599
+ ),
600
+ )
601
+
602
+ // ========================================================================
603
+ // Test: connect failure during create()
604
+ // ========================================================================
605
+
606
+ test(
607
+ "server that fails to connect is marked as failed",
608
+ withInstance(
609
+ {
610
+ "fail-connect": {
611
+ type: "local",
612
+ command: ["echo", "test"],
613
+ },
614
+ },
615
+ (mcp) =>
616
+ Effect.gen(function* () {
617
+ lastCreatedClientName = "fail-connect"
618
+ getOrCreateClientState("fail-connect")
619
+ connectShouldFail = true
620
+ connectError = "Connection refused"
621
+
622
+ yield* mcp.add("fail-connect", {
623
+ type: "local",
624
+ command: ["echo", "test"],
625
+ })
626
+
627
+ const status = yield* mcp.status()
628
+ expect(status["fail-connect"]?.status).toBe("failed")
629
+ if (status["fail-connect"]?.status === "failed") {
630
+ expect(status["fail-connect"].error).toContain("Connection refused")
631
+ }
632
+
633
+ // No tools should be available
634
+ const tools = yield* mcp.tools()
635
+ expect(Object.keys(tools).length).toBe(0)
636
+ }),
637
+ ),
638
+ )
639
+
640
+ // ========================================================================
641
+ // Bug #5: McpOAuthCallback.cancelPending uses wrong key
642
+ // ========================================================================
643
+
644
+ test("McpOAuthCallback.cancelPending is keyed by mcpName but pendingAuths uses oauthState", async () => {
645
+ const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
646
+
647
+ // Register a pending auth with an oauthState key, associated to an mcpName
648
+ const oauthState = "abc123hexstate"
649
+ const callbackPromise = McpOAuthCallback.waitForCallback(oauthState, "my-mcp-server")
650
+
651
+ // cancelPending is called with mcpName — should find the entry via reverse index
652
+ McpOAuthCallback.cancelPending("my-mcp-server")
653
+
654
+ // The callback should still be pending because cancelPending looked up
655
+ // "my-mcp-server" in a map keyed by "abc123hexstate"
656
+ let rejected = false
657
+ callbackPromise.then(() => {}).catch(() => (rejected = true))
658
+
659
+ // Give it a tick
660
+ await new Promise((r) => setTimeout(r, 50))
661
+
662
+ // cancelPending("my-mcp-server") should have rejected the pending callback
663
+ expect(rejected).toBe(true)
664
+
665
+ await McpOAuthCallback.stop()
666
+ })
667
+
668
+ // ========================================================================
669
+ // Test: multiple tools from same server get correct name prefixes
670
+ // ========================================================================
671
+
672
+ test(
673
+ "tools() prefixes tool names with sanitized server name",
674
+ withInstance(
675
+ {
676
+ "my.special-server": {
677
+ type: "local",
678
+ command: ["echo", "test"],
679
+ },
680
+ },
681
+ (mcp) =>
682
+ Effect.gen(function* () {
683
+ lastCreatedClientName = "my.special-server"
684
+ const serverState = getOrCreateClientState("my.special-server")
685
+ serverState.tools = [
686
+ { name: "tool-a", description: "Tool A", inputSchema: { type: "object", properties: {} } },
687
+ { name: "tool.b", description: "Tool B", inputSchema: { type: "object", properties: {} } },
688
+ ]
689
+
690
+ yield* mcp.add("my.special-server", {
691
+ type: "local",
692
+ command: ["echo", "test"],
693
+ })
694
+
695
+ const tools = yield* mcp.tools()
696
+ const keys = Object.keys(tools)
697
+
698
+ // Server name dots should be replaced with underscores
699
+ expect(keys.some((k) => k.startsWith("my_special-server_"))).toBe(true)
700
+ // Tool name dots should be replaced with underscores
701
+ expect(keys.some((k) => k.endsWith("tool_b"))).toBe(true)
702
+ expect(keys.length).toBe(2)
703
+ }),
704
+ ),
705
+ )
706
+
707
+ // ========================================================================
708
+ // Test: transport leak — local stdio timeout (#19168)
709
+ // ========================================================================
710
+
711
+ test(
712
+ "local stdio transport is closed when connect times out (no process leak)",
713
+ withInstance({}, (mcp) =>
714
+ Effect.gen(function* () {
715
+ lastCreatedClientName = "hanging-server"
716
+ getOrCreateClientState("hanging-server")
717
+ connectShouldHang = true
718
+
719
+ const addResult = yield* mcp.add("hanging-server", {
720
+ type: "local",
721
+ command: ["node", "fake.js"],
722
+ timeout: 100,
723
+ })
724
+
725
+ const serverStatus = (addResult.status as any)["hanging-server"] ?? addResult.status
726
+ expect(serverStatus.status).toBe("failed")
727
+ expect(serverStatus.error).toContain("timed out")
728
+ // Transport must be closed to avoid orphaned child process
729
+ expect(transportCloseCount).toBeGreaterThanOrEqual(1)
730
+ }),
731
+ ),
732
+ )
733
+
734
+ // ========================================================================
735
+ // Test: transport leak — remote timeout (#19168)
736
+ // ========================================================================
737
+
738
+ test(
739
+ "remote transport is closed when connect times out",
740
+ withInstance({}, (mcp) =>
741
+ Effect.gen(function* () {
742
+ lastCreatedClientName = "hanging-remote"
743
+ getOrCreateClientState("hanging-remote")
744
+ connectShouldHang = true
745
+
746
+ const addResult = yield* mcp.add("hanging-remote", {
747
+ type: "remote",
748
+ url: "http://localhost:9999/mcp",
749
+ timeout: 100,
750
+ oauth: false,
751
+ })
752
+
753
+ const serverStatus = (addResult.status as any)["hanging-remote"] ?? addResult.status
754
+ expect(serverStatus.status).toBe("failed")
755
+ // Transport must be closed to avoid leaked HTTP connections
756
+ expect(transportCloseCount).toBeGreaterThanOrEqual(1)
757
+ }),
758
+ ),
759
+ )
760
+
761
+ // ========================================================================
762
+ // Test: transport leak — failed remote transports not closed (#19168)
763
+ // ========================================================================
764
+
765
+ test(
766
+ "failed remote transport is closed before trying next transport",
767
+ withInstance({}, (mcp) =>
768
+ Effect.gen(function* () {
769
+ lastCreatedClientName = "fail-remote"
770
+ getOrCreateClientState("fail-remote")
771
+ connectShouldFail = true
772
+ connectError = "Connection refused"
773
+
774
+ const addResult = yield* mcp.add("fail-remote", {
775
+ type: "remote",
776
+ url: "http://localhost:9999/mcp",
777
+ timeout: 5000,
778
+ oauth: false,
779
+ })
780
+
781
+ const serverStatus = (addResult.status as any)["fail-remote"] ?? addResult.status
782
+ expect(serverStatus.status).toBe("failed")
783
+ // Both StreamableHTTP and SSE transports should be closed
784
+ expect(transportCloseCount).toBeGreaterThanOrEqual(2)
785
+ }),
786
+ ),
787
+ )