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,367 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { RemoteWS } from "../../../src/sessions/remote-ws"
3
+ import type { ServerWebSocket } from "bun"
4
+
5
+ function nolog() {
6
+ return {
7
+ info: () => {},
8
+ error: () => {},
9
+ warn: () => {},
10
+ }
11
+ }
12
+
13
+ function capture() {
14
+ const calls: unknown[][] = []
15
+ return {
16
+ calls,
17
+ log: {
18
+ info: (...args: unknown[]) => calls.push(args),
19
+ error: (...args: unknown[]) => calls.push(args),
20
+ warn: (...args: unknown[]) => calls.push(args),
21
+ },
22
+ }
23
+ }
24
+
25
+ function createServer() {
26
+ const messages: string[] = []
27
+ const clients: ServerWebSocket<unknown>[] = []
28
+ const pending: {
29
+ connect: ((ws: ServerWebSocket<unknown>) => void)[]
30
+ message: ((msg: string) => void)[]
31
+ } = { connect: [], message: [] }
32
+
33
+ const server = Bun.serve({
34
+ port: 0,
35
+ fetch(req, server) {
36
+ const upgraded = server.upgrade(req)
37
+ if (!upgraded) return new Response("Not found", { status: 404 })
38
+ return undefined
39
+ },
40
+ websocket: {
41
+ open(ws) {
42
+ clients.push(ws)
43
+ const cb = pending.connect.shift()
44
+ cb?.(ws)
45
+ },
46
+ message(_ws, msg) {
47
+ const str = String(msg)
48
+ messages.push(str)
49
+ const cb = pending.message.shift()
50
+ cb?.(str)
51
+ },
52
+ close(ws) {
53
+ const idx = clients.indexOf(ws)
54
+ if (idx >= 0) clients.splice(idx, 1)
55
+ },
56
+ },
57
+ })
58
+
59
+ return {
60
+ url: `ws://localhost:${server.port}`,
61
+ messages,
62
+ clients,
63
+ stop: () => server.stop(true),
64
+ waitForConnect: () =>
65
+ new Promise<ServerWebSocket<unknown>>((resolve) => {
66
+ pending.connect.push(resolve)
67
+ }),
68
+ waitForMessage: () =>
69
+ new Promise<string>((resolve) => {
70
+ pending.message.push(resolve)
71
+ }),
72
+ }
73
+ }
74
+
75
+ async function settled() {
76
+ await Bun.sleep(20)
77
+ }
78
+
79
+ describe("RemoteWS", () => {
80
+ let server: ReturnType<typeof createServer>
81
+ let conn: RemoteWS.Connection | undefined
82
+
83
+ afterEach(() => {
84
+ conn?.close()
85
+ conn = undefined
86
+ server?.stop()
87
+ })
88
+
89
+ test("connects and sends heartbeat", async () => {
90
+ server = createServer()
91
+ const connecting = server.waitForConnect()
92
+ const msg = server.waitForMessage()
93
+
94
+ conn = RemoteWS.connect({
95
+ url: server.url,
96
+ getToken: async () => "tok",
97
+ getSessions: async () => ({ sessions: [{ id: "s1", status: "active", title: "Test" }] }),
98
+ log: nolog(),
99
+ heartbeat: 100,
100
+ })
101
+
102
+ await connecting
103
+ await settled()
104
+ expect(conn.connected).toBe(true)
105
+
106
+ const raw = await msg
107
+ const parsed = JSON.parse(raw)
108
+ expect(parsed.type).toBe("heartbeat")
109
+ expect(parsed.sessions).toEqual([{ id: "s1", status: "active", title: "Test" }])
110
+ })
111
+
112
+ test("buffers when disconnected, flushes on reconnect", async () => {
113
+ server = createServer()
114
+ const connecting = server.waitForConnect()
115
+
116
+ conn = RemoteWS.connect({
117
+ url: server.url,
118
+ getToken: async () => "tok",
119
+ getSessions: async () => ({ sessions: [] }),
120
+ log: nolog(),
121
+ heartbeat: 60_000,
122
+ })
123
+
124
+ await connecting
125
+ await settled()
126
+
127
+ for (const ws of [...server.clients]) ws.close()
128
+ await Bun.sleep(50)
129
+
130
+ expect(conn.connected).toBe(false)
131
+
132
+ conn.send({ type: "event", sessionId: "s1", event: "test", data: { a: 1 } })
133
+ conn.send({ type: "event", sessionId: "s2", event: "test", data: { b: 2 } })
134
+
135
+ const msg1 = server.waitForMessage()
136
+ const msg2 = server.waitForMessage()
137
+ await server.waitForConnect()
138
+ await settled()
139
+
140
+ const r1 = JSON.parse(await msg1)
141
+ const r2 = JSON.parse(await msg2)
142
+ expect(r1.sessionId).toBe("s1")
143
+ expect(r2.sessionId).toBe("s2")
144
+ })
145
+
146
+ test("reconnects with backoff after server close", async () => {
147
+ server = createServer()
148
+
149
+ conn = RemoteWS.connect({
150
+ url: server.url,
151
+ getToken: async () => "tok",
152
+ getSessions: async () => ({ sessions: [] }),
153
+ log: nolog(),
154
+ heartbeat: 60_000,
155
+ })
156
+
157
+ const ws1 = await server.waitForConnect()
158
+ await settled()
159
+
160
+ const reconnecting = server.waitForConnect()
161
+ ws1.close()
162
+ await Bun.sleep(50)
163
+
164
+ expect(conn.connected).toBe(false)
165
+
166
+ const ws2 = await reconnecting
167
+ expect(ws2).toBeDefined()
168
+ await settled()
169
+ expect(conn.connected).toBe(true)
170
+ })
171
+
172
+ test("stops reconnecting on 4401", async () => {
173
+ server = createServer()
174
+
175
+ conn = RemoteWS.connect({
176
+ url: server.url,
177
+ getToken: async () => "tok",
178
+ getSessions: async () => ({ sessions: [] }),
179
+ log: nolog(),
180
+ heartbeat: 60_000,
181
+ })
182
+
183
+ const ws1 = await server.waitForConnect()
184
+ await settled()
185
+
186
+ ws1.close(4401, "unauthorized")
187
+
188
+ await Bun.sleep(2000)
189
+
190
+ expect(conn.connected).toBe(false)
191
+ expect(server.clients.length).toBe(0)
192
+ })
193
+
194
+ test("onClose callback fires on permanent close", async () => {
195
+ server = createServer()
196
+ const codes: number[] = []
197
+
198
+ conn = RemoteWS.connect({
199
+ url: server.url,
200
+ getToken: async () => "tok",
201
+ getSessions: async () => ({ sessions: [] }),
202
+ log: nolog(),
203
+ heartbeat: 60_000,
204
+ onClose: (code) => codes.push(code),
205
+ })
206
+
207
+ const ws1 = await server.waitForConnect()
208
+ await settled()
209
+
210
+ ws1.close(4401, "unauthorized")
211
+ await Bun.sleep(100)
212
+
213
+ expect(codes).toEqual([4401])
214
+ expect(conn.connected).toBe(false)
215
+ })
216
+
217
+ test("incoming message delivered to onMessage", async () => {
218
+ server = createServer()
219
+ const received: unknown[] = []
220
+ const cap = capture()
221
+ const secret = "user secret prompt"
222
+
223
+ conn = RemoteWS.connect({
224
+ url: server.url,
225
+ getToken: async () => "tok",
226
+ getSessions: async () => ({ sessions: [] }),
227
+ log: cap.log,
228
+ heartbeat: 60_000,
229
+ onMessage: (msg) => received.push(msg),
230
+ })
231
+
232
+ const ws = await server.waitForConnect()
233
+ await settled()
234
+
235
+ ws.send(
236
+ JSON.stringify({
237
+ type: "command",
238
+ id: "c1",
239
+ command: "send_message",
240
+ sessionId: "s1",
241
+ data: { text: secret },
242
+ }),
243
+ )
244
+
245
+ await Bun.sleep(50)
246
+ expect(received.length).toBe(1)
247
+ expect(received[0]).toEqual({
248
+ type: "command",
249
+ id: "c1",
250
+ command: "send_message",
251
+ sessionId: "s1",
252
+ data: { text: secret },
253
+ })
254
+
255
+ const seen = JSON.stringify(cap.calls)
256
+ expect(seen.includes(secret)).toBe(false)
257
+ expect(cap.calls).toContainEqual(["remote-ws received", { bytes: expect.any(Number), type: "command", id: "c1" }])
258
+ })
259
+
260
+ test("close() prevents further reconnection and stops heartbeat", async () => {
261
+ server = createServer()
262
+
263
+ conn = RemoteWS.connect({
264
+ url: server.url,
265
+ getToken: async () => "tok",
266
+ getSessions: async () => ({ sessions: [{ id: "s1", status: "active", title: "Test" }] }),
267
+ log: nolog(),
268
+ heartbeat: 100,
269
+ })
270
+
271
+ await server.waitForConnect()
272
+ await settled()
273
+
274
+ // Drain initial heartbeat message(s)
275
+ server.messages.length = 0
276
+
277
+ conn.close()
278
+ conn = undefined
279
+
280
+ // Wait long enough for heartbeat and reconnect if they were still running
281
+ await Bun.sleep(500)
282
+
283
+ // No new connections and no new heartbeat messages
284
+ expect(server.clients.length).toBe(0)
285
+ expect(server.messages.length).toBe(0)
286
+ })
287
+
288
+ test("force-reconnects on activity timeout", async () => {
289
+ server = createServer()
290
+ const ws1 = server.waitForConnect()
291
+
292
+ conn = RemoteWS.connect({
293
+ url: server.url,
294
+ getToken: async () => "tok",
295
+ getSessions: async () => ({ sessions: [] }),
296
+ log: nolog(),
297
+ heartbeat: 60_000,
298
+ timeout: 200,
299
+ })
300
+
301
+ await ws1
302
+ await settled()
303
+ expect(conn.connected).toBe(true)
304
+
305
+ // Don't send any server messages — timeout should fire
306
+ const ws2 = server.waitForConnect()
307
+ await Bun.sleep(450)
308
+
309
+ // Should have reconnected
310
+ await ws2
311
+ await settled()
312
+ expect(conn.connected).toBe(true)
313
+ })
314
+
315
+ test("resets activity timer on incoming messages", async () => {
316
+ server = createServer()
317
+ const ws1p = server.waitForConnect()
318
+
319
+ conn = RemoteWS.connect({
320
+ url: server.url,
321
+ getToken: async () => "tok",
322
+ getSessions: async () => ({ sessions: [] }),
323
+ log: nolog(),
324
+ heartbeat: 60_000,
325
+ timeout: 300,
326
+ })
327
+
328
+ const ws1 = await ws1p
329
+ await settled()
330
+
331
+ // Send server messages at 100ms intervals — each resets the timer
332
+ for (let i = 0; i < 4; i++) {
333
+ await Bun.sleep(100)
334
+ ws1.send(JSON.stringify({ type: "subscribe", sessionId: `s${i}` }))
335
+ }
336
+
337
+ await settled()
338
+ // Connection should still be alive — activity kept resetting the timer
339
+ expect(conn.connected).toBe(true)
340
+ expect(server.clients.length).toBe(1)
341
+ })
342
+
343
+ test("activity timeout uses custom timeout option", async () => {
344
+ server = createServer()
345
+ const ws1 = server.waitForConnect()
346
+
347
+ conn = RemoteWS.connect({
348
+ url: server.url,
349
+ getToken: async () => "tok",
350
+ getSessions: async () => ({ sessions: [] }),
351
+ log: nolog(),
352
+ heartbeat: 60_000,
353
+ timeout: 100,
354
+ })
355
+
356
+ await ws1
357
+ await settled()
358
+
359
+ // With 100ms timeout, should reconnect faster than default 30s
360
+ const ws2 = server.waitForConnect()
361
+ await Bun.sleep(250)
362
+
363
+ await ws2
364
+ await settled()
365
+ expect(conn.connected).toBe(true)
366
+ })
367
+ })
@@ -0,0 +1,181 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
2
+ import { tmpdir } from "../../fixture/fixture"
3
+ import { Auth } from "../../../src/auth"
4
+ import { Vcs } from "../../../src/project/vcs"
5
+ import { RemoteWS } from "../../../src/kilo-sessions/remote-ws"
6
+ import { RemoteSender } from "../../../src/kilo-sessions/remote-sender"
7
+ import { clearInFlightCache } from "../../../src/kilo-sessions/inflight-cache"
8
+ import { KiloSessions } from "../../../src/kilo-sessions/kilo-sessions"
9
+ import { Instance } from "../../../src/project/instance"
10
+ import { Bus } from "../../../src/bus"
11
+
12
+ const state = {
13
+ connects: 0,
14
+ closes: 0,
15
+ disposes: 0,
16
+ userStatus: 200,
17
+ gate: Promise.resolve(),
18
+ userError: false,
19
+ }
20
+
21
+ describe("KiloSessions.enableRemote", () => {
22
+ beforeEach(() => {
23
+ state.connects = 0
24
+ state.closes = 0
25
+ state.disposes = 0
26
+ state.userStatus = 200
27
+ state.gate = Promise.resolve()
28
+ state.userError = false
29
+ process.env["KILO_DISABLE_SESSION_INGEST"] = "0"
30
+ delete process.env["SAEEOLCODE_SESSION_INGEST_URL"]
31
+
32
+ spyOn(Auth, "get").mockResolvedValue({ type: "api", key: "tok" } as any)
33
+ spyOn(Vcs, "branch").mockResolvedValue("main")
34
+ spyOn(RemoteWS, "connect").mockImplementation(
35
+ () =>
36
+ ({
37
+ connectionId: `conn-${++state.connects}`,
38
+ send() {},
39
+ close() {
40
+ state.closes += 1
41
+ },
42
+ get connected() {
43
+ return true
44
+ },
45
+ }) as RemoteWS.Connection,
46
+ )
47
+ spyOn(RemoteSender, "create").mockImplementation(
48
+ () =>
49
+ ({
50
+ handle() {},
51
+ dispose() {
52
+ state.disposes += 1
53
+ },
54
+ }) as RemoteSender.Sender,
55
+ )
56
+
57
+ // Clear inflight caches so each test gets fresh Auth.get / authValid calls
58
+ clearInFlightCache("kilo-sessions:token")
59
+ clearInFlightCache("kilo-sessions:token-valid:tok")
60
+
61
+ globalThis.fetch = mock(async (input) => {
62
+ await state.gate
63
+ if (String(input).endsWith("/api/user")) {
64
+ if (state.userError) throw new Error("network down")
65
+ return new Response(null, { status: state.userStatus })
66
+ }
67
+ return new Response(null, { status: 200 })
68
+ }) as unknown as typeof fetch
69
+ })
70
+
71
+ afterEach(() => {
72
+ // Stub Bus.publish so disableRemote's fire-and-forget publish doesn't reject
73
+ // outside an Instance context (it needs InstanceState which requires ALS).
74
+ const pub = spyOn(Bus, "publish").mockResolvedValue(undefined as never)
75
+ KiloSessions.disableRemote()
76
+ pub.mockRestore()
77
+ mock.restore()
78
+ })
79
+
80
+ test("concurrent enableRemote shares one connection", async () => {
81
+ await using tmp = await tmpdir({ git: true })
82
+
83
+ await Instance.provide({
84
+ directory: tmp.path,
85
+ fn: async () => {
86
+ await Promise.all([KiloSessions.enableRemote(), KiloSessions.enableRemote(), KiloSessions.enableRemote()])
87
+ expect(state.connects).toBe(1)
88
+ expect(KiloSessions.remoteStatus()).toEqual({ enabled: true, connected: true })
89
+ },
90
+ })
91
+ })
92
+
93
+ test("enableRemote fails when token is invalid", async () => {
94
+ state.userStatus = 401
95
+ await using tmp = await tmpdir({ git: true })
96
+
97
+ KiloSessions.disableRemote()
98
+ clearInFlightCache("kilo-sessions:token-valid:tok")
99
+
100
+ await Instance.provide({
101
+ directory: tmp.path,
102
+ fn: async () => {
103
+ await expect(KiloSessions.enableRemote()).rejects.toThrow(
104
+ "Unable to enable remote: invalid or expired Kilo credentials. Run `kilo auth login`.",
105
+ )
106
+ expect(state.connects).toBe(0)
107
+ expect(KiloSessions.remoteStatus()).toEqual({ enabled: false, connected: false })
108
+ },
109
+ })
110
+ })
111
+
112
+ test("disableRemote cancels in-flight enableRemote", async () => {
113
+ let release = () => {}
114
+ state.gate = new Promise<void>((resolve) => {
115
+ release = resolve
116
+ })
117
+
118
+ await using tmp = await tmpdir({ git: true })
119
+
120
+ clearInFlightCache("kilo-sessions:token-valid:tok")
121
+
122
+ await Instance.provide({
123
+ directory: tmp.path,
124
+ fn: async () => {
125
+ const pending = KiloSessions.enableRemote()
126
+ KiloSessions.disableRemote()
127
+ release()
128
+ await pending
129
+ expect(state.connects).toBe(1)
130
+ expect(state.disposes).toBe(1)
131
+ expect(state.closes).toBe(1)
132
+ expect(KiloSessions.remoteStatus()).toEqual({ enabled: false, connected: false })
133
+ },
134
+ })
135
+ })
136
+
137
+ test("transient auth check failure is retryable and does not connect", async () => {
138
+ state.userError = true
139
+ await using tmp = await tmpdir({ git: true })
140
+
141
+ KiloSessions.disableRemote()
142
+ clearInFlightCache("kilo-sessions:token-valid:tok")
143
+
144
+ await Instance.provide({
145
+ directory: tmp.path,
146
+ fn: async () => {
147
+ await expect(KiloSessions.enableRemote()).rejects.toThrow(
148
+ "Unable to enable remote: failed to verify Kilo credentials.",
149
+ )
150
+ expect(state.connects).toBe(0)
151
+ expect(KiloSessions.remoteStatus()).toEqual({ enabled: false, connected: false })
152
+ },
153
+ })
154
+ })
155
+
156
+ test("disable then re-enable replaces stale pending connection", async () => {
157
+ let release = () => {}
158
+ state.gate = new Promise<void>((resolve) => {
159
+ release = resolve
160
+ })
161
+
162
+ await using tmp = await tmpdir({ git: true })
163
+
164
+ clearInFlightCache("kilo-sessions:token-valid:tok")
165
+
166
+ await Instance.provide({
167
+ directory: tmp.path,
168
+ fn: async () => {
169
+ const first = KiloSessions.enableRemote()
170
+ KiloSessions.disableRemote()
171
+ const second = KiloSessions.enableRemote()
172
+ release()
173
+ await Promise.all([first, second])
174
+ expect(state.connects).toBe(2)
175
+ expect(state.disposes).toBe(1)
176
+ expect(state.closes).toBe(1)
177
+ expect(KiloSessions.remoteStatus()).toEqual({ enabled: true, connected: true })
178
+ },
179
+ })
180
+ })
181
+ })
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Regression test for the Slot wrapper in plugin/slots.tsx.
3
+ *
4
+ * This test locks in two things:
5
+ * 1. A static invariant: the wrapper does NOT spread raw props (`...props`),
6
+ * which would silently reintroduce the regression on refactors.
7
+ * 2. A runtime check: forwarding props through the same pattern used in
8
+ * slots.tsx preserves reactivity for arbitrary props (not just children).
9
+ */
10
+
11
+ import { describe, expect, test } from "bun:test"
12
+ import fs from "node:fs"
13
+ import path from "node:path"
14
+ import { children, createEffect, createRoot, createSignal, mergeProps } from "solid-js"
15
+
16
+ const SLOTS_FILE = path.resolve(import.meta.dir, "../../src/cli/cmd/tui/plugin/slots.tsx")
17
+
18
+ describe("Slot wrapper preserves prop reactivity", () => {
19
+ test("slots.tsx does not use `{...props}` spread to forward props", () => {
20
+ // Spread on a plain object in Solid evaluates every prop once and freezes
21
+ // it. mergeProps (or a getter per prop) is required to keep reactivity.
22
+ const content = fs.readFileSync(SLOTS_FILE, "utf-8")
23
+ const wrapper = content.match(/export const Slot[\s\S]*?^}/m)?.[0] ?? ""
24
+ expect(wrapper).not.toBe("")
25
+ expect(wrapper).not.toMatch(/\.\.\.props/)
26
+ })
27
+
28
+ test("slots.tsx forwards props through mergeProps (or per-prop getters)", () => {
29
+ const content = fs.readFileSync(SLOTS_FILE, "utf-8")
30
+ const wrapper = content.match(/export const Slot[\s\S]*?^}/m)?.[0] ?? ""
31
+ const usesMergeProps = /mergeProps\s*\(/.test(wrapper)
32
+ expect(usesMergeProps).toBe(true)
33
+ })
34
+
35
+ test("mergeProps preserves reactivity of non-children props", () => {
36
+ // Simulates the exact pattern used in slots.tsx: resolve children via the
37
+ // `children()` helper and forward the rest via mergeProps. Non-children
38
+ // reactive props (like `visible`, `disabled`, `ref`) must keep tracking
39
+ // their source signals — otherwise the slot-internal consumer (opentui
40
+ // registry → plugin) sees a frozen initial value.
41
+ const [visible, setVisible] = createSignal(true)
42
+ const [disabled, setDisabled] = createSignal(false)
43
+ const refCalls: Array<string> = []
44
+ const refA = () => refCalls.push("a")
45
+ const refB = () => refCalls.push("b")
46
+ const [ref, setRef] = createSignal<() => void>(refA)
47
+
48
+ const seen: Array<{ visible: boolean; disabled: boolean }> = []
49
+ const refSeen: Array<() => void> = []
50
+
51
+ const dispose = createRoot((dispose) => {
52
+ // Pretend JSX: reactive props passed into the Slot wrapper.
53
+ const sourceProps = {
54
+ get visible() {
55
+ return visible()
56
+ },
57
+ get disabled() {
58
+ return disabled()
59
+ },
60
+ get ref() {
61
+ return ref()
62
+ },
63
+ children: "unused",
64
+ }
65
+
66
+ // This mirrors plugin/slots.tsx exactly.
67
+ const value = children(() => sourceProps.children)
68
+ const merged = mergeProps(sourceProps, {
69
+ get children() {
70
+ return value()
71
+ },
72
+ }) as typeof sourceProps
73
+
74
+ createEffect(() => {
75
+ seen.push({ visible: merged.visible, disabled: merged.disabled })
76
+ })
77
+ createEffect(() => {
78
+ refSeen.push(merged.ref)
79
+ })
80
+
81
+ return dispose
82
+ })
83
+
84
+ // Initial render tracked.
85
+ expect(seen).toEqual([{ visible: true, disabled: false }])
86
+ expect(refSeen.length).toBe(1)
87
+ expect(refSeen[0]).toBe(refA)
88
+
89
+ // Flip the source signals — the merged view must update.
90
+ setVisible(false)
91
+ expect(seen).toEqual([
92
+ { visible: true, disabled: false },
93
+ { visible: false, disabled: false },
94
+ ])
95
+
96
+ setDisabled(true)
97
+ expect(seen[seen.length - 1]).toEqual({ visible: false, disabled: true })
98
+
99
+ // Ref callback must also track through the wrapper — this is what makes
100
+ // the session prompt ref={bind} actually attach/re-attach correctly.
101
+ setRef(() => refB)
102
+ expect(refSeen.length).toBe(2)
103
+ expect(refSeen[1]).toBe(refB)
104
+
105
+ dispose()
106
+ })
107
+
108
+ test("plain `{...props}` spread does NOT preserve reactivity (proves the regression)", () => {
109
+ // Negative control: the exact bug we're guarding against. A spread into a
110
+ // plain object decouples the reactive source, so an effect on the copy
111
+ // only fires once.
112
+ const [visible, setVisible] = createSignal(true)
113
+ let fires = 0
114
+
115
+ const dispose = createRoot((dispose) => {
116
+ const sourceProps = {
117
+ get visible() {
118
+ return visible()
119
+ },
120
+ }
121
+
122
+ // BUG pattern — copies the value at evaluation time.
123
+ const frozen = { ...sourceProps } as { visible: boolean }
124
+
125
+ createEffect(() => {
126
+ // Touch frozen.visible to subscribe (but it's a static property now).
127
+ void frozen.visible
128
+ fires++
129
+ })
130
+
131
+ return dispose
132
+ })
133
+
134
+ expect(fires).toBe(1)
135
+ setVisible(false)
136
+ // A correctly reactive wrapper would have fired again; the frozen copy
137
+ // does not. Keeping this assertion documents why mergeProps is required.
138
+ expect(fires).toBe(1)
139
+
140
+ dispose()
141
+ })
142
+ })