saeeol 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (545) hide show
  1. package/AGENTS.md +72 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/assets/saeeol.ico +0 -0
  5. package/bin/saeeol.cjs +3 -1
  6. package/bunfig.toml +7 -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/preflight.ts +138 -0
  112. package/src/cli/cmd/tui/thread.ts +20 -0
  113. package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
  114. package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
  115. package/src/overlay/cost-tracker/format.ts +1 -1
  116. package/src/overlay/cost-tracker/index.ts +4 -4
  117. package/src/overlay/cost-tracker/state.ts +2 -2
  118. package/src/overlay/cost-tracker/types.ts +2 -2
  119. package/src/overlay/memory/age.ts +1 -1
  120. package/src/overlay/memory/index.ts +4 -4
  121. package/src/overlay/memory/paths.ts +2 -2
  122. package/src/overlay/memory/scan.ts +1 -1
  123. package/src/overlay/memory/types.ts +2 -2
  124. package/src/overlay/tool/bash-security.ts +3 -3
  125. package/src/overlay/util/url.ts +1 -1
  126. package/src/plugin/codex-auth.ts +1 -1
  127. package/src/provider/model-cache.ts +2 -2
  128. package/src/provider/provider-resolve.ts +3 -3
  129. package/src/provider/transform-message.ts +1 -1
  130. package/src/server/routes/game.ts +284 -0
  131. package/src/server/server.ts +2 -0
  132. package/src/session/core/compaction/compaction-helpers.ts +1 -1
  133. package/src/session/core/compaction/compaction.ts +1 -1
  134. package/src/session/core/session.ts +2 -0
  135. package/src/sessions/ingest-queue.ts +2 -2
  136. package/src/sessions/remote-ws.ts +1 -1
  137. package/src/tool/workflow/question.ts +1 -1
  138. package/src/util/abort.ts +1 -1
  139. package/src/util/bom.ts +2 -2
  140. package/src/util/color.ts +1 -1
  141. package/src/util/data-url.ts +1 -1
  142. package/src/util/defer.ts +1 -1
  143. package/src/util/error.ts +2 -2
  144. package/src/util/filesystem.ts +2 -2
  145. package/src/util/format.ts +1 -1
  146. package/src/util/iife.ts +1 -1
  147. package/src/util/local-context.ts +1 -1
  148. package/src/util/locale.ts +2 -2
  149. package/src/util/lock.ts +1 -1
  150. package/src/util/network.ts +1 -1
  151. package/src/util/signal.ts +1 -1
  152. package/src/util/token.ts +1 -1
  153. package/src/util/wildcard.ts +1 -1
  154. package/sst-env.d.ts +10 -0
  155. package/test/AGENTS.md +133 -0
  156. package/test/account/repo.test.ts +352 -0
  157. package/test/account/service.test.ts +456 -0
  158. package/test/acp/agent-interface.test.ts +51 -0
  159. package/test/acp/event-subscription.test.ts +725 -0
  160. package/test/agent/agent.test.ts +890 -0
  161. package/test/auth/auth.test.ts +86 -0
  162. package/test/bun/registry.test.ts +75 -0
  163. package/test/bus/bus-effect.test.ts +161 -0
  164. package/test/bus/bus-integration.test.ts +87 -0
  165. package/test/bus/bus.test.ts +219 -0
  166. package/test/cli/account.test.ts +26 -0
  167. package/test/cli/auto-mode.test.ts +75 -0
  168. package/test/cli/bin-saeeol.test.ts +8 -0
  169. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  170. package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
  171. package/test/cli/cmd/tui/sync.test.tsx +159 -0
  172. package/test/cli/error.test.ts +18 -0
  173. package/test/cli/github-action.test.ts +198 -0
  174. package/test/cli/github-remote.test.ts +85 -0
  175. package/test/cli/import.test.ts +97 -0
  176. package/test/cli/install-artifact.test.ts +72 -0
  177. package/test/cli/plugin-auth-picker.test.ts +120 -0
  178. package/test/cli/pr.test.ts +59 -0
  179. package/test/cli/tui/editor-context-zed.test.ts +356 -0
  180. package/test/cli/tui/editor-context.test.tsx +228 -0
  181. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  182. package/test/cli/tui/markdown.test.ts +161 -0
  183. package/test/cli/tui/plugin-add.test.ts +111 -0
  184. package/test/cli/tui/plugin-install.test.ts +87 -0
  185. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  186. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  187. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  188. package/test/cli/tui/plugin-loader.test.ts +816 -0
  189. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  190. package/test/cli/tui/revert-diff.test.ts +35 -0
  191. package/test/cli/tui/slot-replace.test.tsx +47 -0
  192. package/test/cli/tui/theme-store.test.ts +54 -0
  193. package/test/cli/tui/thread.test.ts +28 -0
  194. package/test/cli/tui/transcript.test.ts +426 -0
  195. package/test/cli/tui/usage.test.ts +60 -0
  196. package/test/cli/tui/use-event.test.tsx +175 -0
  197. package/test/config/agent-color.test.ts +67 -0
  198. package/test/config/config.test.ts +2544 -0
  199. package/test/config/fixtures/empty-frontmatter.md +4 -0
  200. package/test/config/fixtures/frontmatter.md +28 -0
  201. package/test/config/fixtures/markdown-header.md +11 -0
  202. package/test/config/fixtures/no-frontmatter.md +1 -0
  203. package/test/config/fixtures/weird-model-id.md +13 -0
  204. package/test/config/lsp.test.ts +87 -0
  205. package/test/config/markdown.test.ts +228 -0
  206. package/test/config/plugin.test.ts +0 -0
  207. package/test/config/tui.test.ts +624 -0
  208. package/test/control-plane/adapters.test.ts +71 -0
  209. package/test/control-plane/workspace.test.ts +1526 -0
  210. package/test/effect/app-runtime-logger.test.ts +98 -0
  211. package/test/effect/config-service.test.ts +65 -0
  212. package/test/effect/instance-state.test.ts +394 -0
  213. package/test/effect/run-service.test.ts +89 -0
  214. package/test/effect/runner.test.ts +523 -0
  215. package/test/fake/provider.ts +82 -0
  216. package/test/file/fsmonitor.test.ts +68 -0
  217. package/test/file/ignore.test.ts +10 -0
  218. package/test/file/index.test.ts +954 -0
  219. package/test/file/path-traversal.test.ts +205 -0
  220. package/test/file/ripgrep.test.ts +226 -0
  221. package/test/file/watcher.test.ts +249 -0
  222. package/test/filesystem/filesystem.test.ts +319 -0
  223. package/test/fixture/db.ts +11 -0
  224. package/test/fixture/fixture.test.ts +26 -0
  225. package/test/fixture/fixture.ts +175 -0
  226. package/test/fixture/flock-worker.ts +72 -0
  227. package/test/fixture/log-init-worker.ts +62 -0
  228. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  229. package/test/fixture/plug-worker.ts +93 -0
  230. package/test/fixture/plugin-meta-worker.ts +19 -0
  231. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  232. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  233. package/test/fixture/skills/index.json +6 -0
  234. package/test/fixture/tui-plugin.ts +323 -0
  235. package/test/fixture/tui-runtime.ts +31 -0
  236. package/test/format/format.test.ts +272 -0
  237. package/test/git/git.test.ts +128 -0
  238. package/test/ide/ide.test.ts +82 -0
  239. package/test/installation/installation.test.ts +168 -0
  240. package/test/keybind.test.ts +421 -0
  241. package/test/lib/effect.ts +53 -0
  242. package/test/lib/filesystem.ts +10 -0
  243. package/test/lib/llm-server.ts +778 -0
  244. package/test/lib/websocket.ts +46 -0
  245. package/test/lsp/client.test.ts +482 -0
  246. package/test/lsp/index.test.ts +160 -0
  247. package/test/lsp/launch.test.ts +22 -0
  248. package/test/lsp/lifecycle.test.ts +184 -0
  249. package/test/ltm/ltm.test.ts +230 -0
  250. package/test/mcp/headers.test.ts +178 -0
  251. package/test/mcp/lifecycle.test.ts +787 -0
  252. package/test/mcp/oauth-auto-connect.test.ts +311 -0
  253. package/test/mcp/oauth-browser.test.ts +276 -0
  254. package/test/mcp/oauth-callback.test.ts +34 -0
  255. package/test/memory/abort-leak-webfetch.ts +49 -0
  256. package/test/memory/abort-leak.test.ts +128 -0
  257. package/test/patch/patch.test.ts +348 -0
  258. package/test/permission/arity.test.ts +33 -0
  259. package/test/permission/next.test.ts +1227 -0
  260. package/test/permission/next.toConfig.test.ts +110 -0
  261. package/test/permission-task.test.ts +326 -0
  262. package/test/plugin/auth-override.test.ts +79 -0
  263. package/test/plugin/cloudflare.test.ts +68 -0
  264. package/test/plugin/codex.test.ts +123 -0
  265. package/test/plugin/github-copilot-models.test.ts +261 -0
  266. package/test/plugin/install-concurrency.test.ts +140 -0
  267. package/test/plugin/install.test.ts +570 -0
  268. package/test/plugin/loader-shared.test.ts +1169 -0
  269. package/test/plugin/meta.test.ts +137 -0
  270. package/test/plugin/plugin-contract.test.ts +291 -0
  271. package/test/plugin/shared.test.ts +88 -0
  272. package/test/plugin/trigger.test.ts +102 -0
  273. package/test/plugin/workspace-adapter.test.ts +109 -0
  274. package/test/preload.ts +77 -0
  275. package/test/project/instance.test.ts +276 -0
  276. package/test/project/migrate-global.test.ts +152 -0
  277. package/test/project/project.test.ts +600 -0
  278. package/test/project/vcs.test.ts +286 -0
  279. package/test/project/worktree-remove.test.ts +126 -0
  280. package/test/project/worktree.test.ts +223 -0
  281. package/test/provider/amazon-bedrock.test.ts +462 -0
  282. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  283. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  284. package/test/provider/gitlab-duo.test.ts +413 -0
  285. package/test/provider/local.test.ts +208 -0
  286. package/test/provider/models.test.ts +261 -0
  287. package/test/provider/provider-category.test.ts +190 -0
  288. package/test/provider/provider.test.ts +2758 -0
  289. package/test/provider/transform.test.ts +3681 -0
  290. package/test/pty/pty-output-isolation.test.ts +147 -0
  291. package/test/pty/pty-session.test.ts +102 -0
  292. package/test/pty/pty-shell.test.ts +104 -0
  293. package/test/question/question.test.ts +490 -0
  294. package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
  295. package/test/saeeol/agent-manager-tool.test.ts +71 -0
  296. package/test/saeeol/agent-permission-overrides.test.ts +75 -0
  297. package/test/saeeol/agent-skill-permissions.test.ts +37 -0
  298. package/test/saeeol/ask-agent-permissions.test.ts +303 -0
  299. package/test/saeeol/bash-hierarchy.test.ts +64 -0
  300. package/test/saeeol/bash-permission-metadata.test.ts +66 -0
  301. package/test/saeeol/bash-security-extended.test.ts +243 -0
  302. package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
  303. package/test/saeeol/boxes-integration.test.ts +415 -0
  304. package/test/saeeol/builtin-skills.test.ts +75 -0
  305. package/test/saeeol/cleanup.ts +28 -0
  306. package/test/saeeol/cli/dev-setup.test.ts +74 -0
  307. package/test/saeeol/cli/roll-call.test.ts +161 -0
  308. package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
  309. package/test/saeeol/codex-auth-refresh.test.ts +124 -0
  310. package/test/saeeol/commit-message/generate.test.ts +188 -0
  311. package/test/saeeol/commit-message/git-context.test.ts +303 -0
  312. package/test/saeeol/commit-message-windows.test.ts +38 -0
  313. package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
  314. package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
  315. package/test/saeeol/compaction-skip-guard.test.ts +224 -0
  316. package/test/saeeol/compaction-smart-select.test.ts +100 -0
  317. package/test/saeeol/config/config.test.ts +166 -0
  318. package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
  319. package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
  320. package/test/saeeol/config-gitignore.test.ts +70 -0
  321. package/test/saeeol/config-injector.test.ts +305 -0
  322. package/test/saeeol/config-resilience.test.ts +234 -0
  323. package/test/saeeol/config-validation.test.ts +183 -0
  324. package/test/saeeol/cost-propagation.test.ts +94 -0
  325. package/test/saeeol/cost-tracker-extended.test.ts +141 -0
  326. package/test/saeeol/cost-tracker.test.ts +64 -0
  327. package/test/saeeol/custom-provider-delete.test.ts +149 -0
  328. package/test/saeeol/diff-full.test.ts +226 -0
  329. package/test/saeeol/edit-permission-filediff.test.ts +223 -0
  330. package/test/saeeol/encoding.test.ts +364 -0
  331. package/test/saeeol/enhance-prompt.test.ts +61 -0
  332. package/test/saeeol/ensure-plan-dir.test.ts +32 -0
  333. package/test/saeeol/errors.test.ts +144 -0
  334. package/test/saeeol/external-directory-boundary.test.ts +96 -0
  335. package/test/saeeol/gateway-headers.test.ts +88 -0
  336. package/test/saeeol/help.test.ts +191 -0
  337. package/test/saeeol/ignore-migrator.test.ts +308 -0
  338. package/test/saeeol/indexing-auth.test.ts +45 -0
  339. package/test/saeeol/indexing-feature.test.ts +44 -0
  340. package/test/saeeol/indexing-label.test.ts +70 -0
  341. package/test/saeeol/indexing-startup.test.ts +381 -0
  342. package/test/saeeol/indexing-worktree.test.ts +73 -0
  343. package/test/saeeol/instruction.test.ts +136 -0
  344. package/test/saeeol/lancedb-runtime.test.ts +116 -0
  345. package/test/saeeol/loader-auth.test.ts +168 -0
  346. package/test/saeeol/local-model.test.ts +621 -0
  347. package/test/saeeol/logo.test.ts +31 -0
  348. package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
  349. package/test/saeeol/mcp-branding.test.ts +33 -0
  350. package/test/saeeol/mcp-docker-rm.test.ts +32 -0
  351. package/test/saeeol/mcp-migrator.test.ts +736 -0
  352. package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
  353. package/test/saeeol/memory-io.test.ts +198 -0
  354. package/test/saeeol/memory-paths.test.ts +87 -0
  355. package/test/saeeol/memory-security.test.ts +166 -0
  356. package/test/saeeol/model-cache-org.test.ts +164 -0
  357. package/test/saeeol/model-info-panel-utils.test.ts +52 -0
  358. package/test/saeeol/model-info-panel.types.test.ts +7 -0
  359. package/test/saeeol/models-401-fallback.test.ts +52 -0
  360. package/test/saeeol/modes-migrator.test.ts +320 -0
  361. package/test/saeeol/nvidia-headers.test.ts +74 -0
  362. package/test/saeeol/patch-jsonc.test.ts +73 -0
  363. package/test/saeeol/patch.test.ts +172 -0
  364. package/test/saeeol/paths.test.ts +265 -0
  365. package/test/saeeol/permission/config-paths.test.ts +174 -0
  366. package/test/saeeol/permission/env-read.test.ts +149 -0
  367. package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
  368. package/test/saeeol/permission/next.always-rules.test.ts +882 -0
  369. package/test/saeeol/permission/next.reply-http.test.ts +205 -0
  370. package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
  371. package/test/saeeol/plan-exit-detection.test.ts +494 -0
  372. package/test/saeeol/plan-followup.test.ts +1376 -0
  373. package/test/saeeol/project-config-update.test.ts +120 -0
  374. package/test/saeeol/project-id.test.ts +455 -0
  375. package/test/saeeol/provider-cost.test.ts +171 -0
  376. package/test/saeeol/provider-list-failed-state.test.ts +100 -0
  377. package/test/saeeol/question-dismiss-all.test.ts +174 -0
  378. package/test/saeeol/read-directory.test.ts +116 -0
  379. package/test/saeeol/rules-migrator.test.ts +257 -0
  380. package/test/saeeol/run-auto.test.ts +176 -0
  381. package/test/saeeol/run-network.test.ts +224 -0
  382. package/test/saeeol/semantic-search.test.ts +186 -0
  383. package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
  384. package/test/saeeol/session/instruction-substitution.test.ts +72 -0
  385. package/test/saeeol/session/platform-attribution.test.ts +118 -0
  386. package/test/saeeol/session/session.test.ts +105 -0
  387. package/test/saeeol/session-compaction-cap.test.ts +399 -0
  388. package/test/saeeol/session-compaction-chunks.test.ts +501 -0
  389. package/test/saeeol/session-compaction-safety.test.ts +481 -0
  390. package/test/saeeol/session-fork-remap.test.ts +251 -0
  391. package/test/saeeol/session-import-service.test.ts +114 -0
  392. package/test/saeeol/session-list.test.ts +47 -0
  393. package/test/saeeol/session-message-metadata.test.ts +128 -0
  394. package/test/saeeol/session-overflow.test.ts +78 -0
  395. package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
  396. package/test/saeeol/session-processor-network-offline.test.ts +204 -0
  397. package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
  398. package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
  399. package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
  400. package/test/saeeol/session-prompt-queue.test.ts +815 -0
  401. package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
  402. package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
  403. package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
  404. package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
  405. package/test/saeeol/sessions/remote-ws.test.ts +367 -0
  406. package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
  407. package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
  408. package/test/saeeol/snapshot-cache.test.ts +84 -0
  409. package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
  410. package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
  411. package/test/saeeol/stats-subagent-cost.test.ts +123 -0
  412. package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
  413. package/test/saeeol/suggestion/suggestion.test.ts +145 -0
  414. package/test/saeeol/suggestion/tool.test.ts +298 -0
  415. package/test/saeeol/summary-file-diff.test.ts +28 -0
  416. package/test/saeeol/system-prompt.test.ts +142 -0
  417. package/test/saeeol/task-nesting.test.ts +193 -0
  418. package/test/saeeol/telemetry/feedback.test.ts +8 -0
  419. package/test/saeeol/todo-view.test.ts +57 -0
  420. package/test/saeeol/tool-encoding.test.ts +455 -0
  421. package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
  422. package/test/saeeol/tool-registry-indexing.test.ts +236 -0
  423. package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
  424. package/test/saeeol/tool-task-model.test.ts +352 -0
  425. package/test/saeeol/transform-opus-4.7.test.ts +89 -0
  426. package/test/saeeol/tui-diff.test.ts +91 -0
  427. package/test/saeeol/tui-sync.test.ts +80 -0
  428. package/test/saeeol/util/url.test.ts +141 -0
  429. package/test/saeeol/workflows-migrator.test.ts +261 -0
  430. package/test/saeeol/worktree-diff-summary.test.ts +64 -0
  431. package/test/saeeol/worktree-diff.test.ts +223 -0
  432. package/test/saeeol/worktree-remove-lock.test.ts +82 -0
  433. package/test/server/AGENTS.md +15 -0
  434. package/test/server/contract.test.ts +357 -0
  435. package/test/server/experimental-session-list.test.ts +157 -0
  436. package/test/server/global-session-list.test.ts +155 -0
  437. package/test/server/httpapi-authorization.test.ts +103 -0
  438. package/test/server/httpapi-bridge.test.ts +440 -0
  439. package/test/server/httpapi-config.test.ts +67 -0
  440. package/test/server/httpapi-cors.test.ts +89 -0
  441. package/test/server/httpapi-event.test.ts +57 -0
  442. package/test/server/httpapi-experimental.test.ts +219 -0
  443. package/test/server/httpapi-file.test.ts +79 -0
  444. package/test/server/httpapi-instance-context.test.ts +237 -0
  445. package/test/server/httpapi-instance.legacy.test.ts +140 -0
  446. package/test/server/httpapi-instance.test.ts +83 -0
  447. package/test/server/httpapi-json-parity.test.ts +263 -0
  448. package/test/server/httpapi-mcp-oauth.test.ts +76 -0
  449. package/test/server/httpapi-mcp.test.ts +189 -0
  450. package/test/server/httpapi-provider.test.ts +153 -0
  451. package/test/server/httpapi-pty-websocket.test.ts +16 -0
  452. package/test/server/httpapi-pty.test.ts +175 -0
  453. package/test/server/httpapi-raw-route-auth.test.ts +89 -0
  454. package/test/server/httpapi-sdk.test.ts +681 -0
  455. package/test/server/httpapi-session.test.ts +464 -0
  456. package/test/server/httpapi-sync.test.ts +130 -0
  457. package/test/server/httpapi-tui.test.ts +121 -0
  458. package/test/server/httpapi-workspace-routing.test.ts +471 -0
  459. package/test/server/httpapi-workspace.test.ts +427 -0
  460. package/test/server/lib/conformance.ts +88 -0
  461. package/test/server/lib/stateful.ts +112 -0
  462. package/test/server/project-init-git.test.ts +113 -0
  463. package/test/server/proxy-util.test.ts +113 -0
  464. package/test/server/session-actions.test.ts +49 -0
  465. package/test/server/session-list.test.ts +238 -0
  466. package/test/server/session-messages.test.ts +167 -0
  467. package/test/server/session-select.test.ts +100 -0
  468. package/test/server/trace-attributes.test.ts +76 -0
  469. package/test/server/workspace-proxy.test.ts +165 -0
  470. package/test/server/workspace-routing.test.ts +85 -0
  471. package/test/session/compaction.test.ts +2420 -0
  472. package/test/session/instruction.test.ts +247 -0
  473. package/test/session/llm.test.ts +1273 -0
  474. package/test/session/message-v2.test.ts +1291 -0
  475. package/test/session/messages-pagination.test.ts +1173 -0
  476. package/test/session/network.test.ts +249 -0
  477. package/test/session/processor-effect.test.ts +847 -0
  478. package/test/session/prompt.test.ts +2131 -0
  479. package/test/session/retry.test.ts +340 -0
  480. package/test/session/revert-compact.test.ts +639 -0
  481. package/test/session/schema-decoding.test.ts +311 -0
  482. package/test/session/session-entry-stepper.test.ts +917 -0
  483. package/test/session/session-schema.test.ts +76 -0
  484. package/test/session/snapshot-tool-race.test.ts +257 -0
  485. package/test/session/structured-output-integration.test.ts +265 -0
  486. package/test/session/structured-output.test.ts +381 -0
  487. package/test/session/system.test.ts +73 -0
  488. package/test/share/share-next.test.ts +333 -0
  489. package/test/shell/shell.test.ts +99 -0
  490. package/test/skill/discovery.test.ts +116 -0
  491. package/test/skill/skill.test.ts +393 -0
  492. package/test/smoke/.tui-debug-output.txt +1 -0
  493. package/test/smoke/.tui-debug-plain.txt +1 -0
  494. package/test/smoke/.tui-walkthrough-report.txt +122 -0
  495. package/test/smoke/smoke-tui-pty.test.ts +123 -0
  496. package/test/smoke/smoke-tui.mjs +83 -0
  497. package/test/smoke/tui-walkthrough.test.ts +520 -0
  498. package/test/snapshot/snapshot.test.ts +1531 -0
  499. package/test/storage/db.test.ts +23 -0
  500. package/test/storage/json-migration.test.ts +832 -0
  501. package/test/storage/storage.test.ts +293 -0
  502. package/test/suggestion/suggestion.test.ts +1 -0
  503. package/test/sync/index.test.ts +256 -0
  504. package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
  505. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  506. package/test/tool/apply_patch.test.ts +614 -0
  507. package/test/tool/bash.test.ts +1225 -0
  508. package/test/tool/diagnostics-filter.test.ts +55 -0
  509. package/test/tool/edit.test.ts +754 -0
  510. package/test/tool/external-directory.test.ts +169 -0
  511. package/test/tool/fixtures/large-image.png +0 -0
  512. package/test/tool/fixtures/models-api.json +65179 -0
  513. package/test/tool/glob.test.ts +107 -0
  514. package/test/tool/grep.test.ts +114 -0
  515. package/test/tool/lsp.test.ts +187 -0
  516. package/test/tool/parameters.test.ts +243 -0
  517. package/test/tool/question.test.ts +129 -0
  518. package/test/tool/read.test.ts +500 -0
  519. package/test/tool/recall.test.ts +151 -0
  520. package/test/tool/registry.test.ts +203 -0
  521. package/test/tool/skill.test.ts +135 -0
  522. package/test/tool/suggest.test.ts +1 -0
  523. package/test/tool/task.test.ts +612 -0
  524. package/test/tool/tool-define.test.ts +99 -0
  525. package/test/tool/truncation.test.ts +260 -0
  526. package/test/tool/webfetch.test.ts +103 -0
  527. package/test/tool/write.test.ts +291 -0
  528. package/test/util/data-url.test.ts +14 -0
  529. package/test/util/effect-zod.test.ts +754 -0
  530. package/test/util/error.test.ts +38 -0
  531. package/test/util/filesystem.test.ts +656 -0
  532. package/test/util/format.test.ts +59 -0
  533. package/test/util/glob.test.ts +164 -0
  534. package/test/util/iife.test.ts +36 -0
  535. package/test/util/lazy.test.ts +50 -0
  536. package/test/util/lock.test.ts +72 -0
  537. package/test/util/log.test.ts +86 -0
  538. package/test/util/module.test.ts +59 -0
  539. package/test/util/process.test.ts +128 -0
  540. package/test/util/timeout.test.ts +21 -0
  541. package/test/util/which.test.ts +100 -0
  542. package/test/util/wildcard.test.ts +90 -0
  543. package/test/workspace/workspace-restore.test.ts +296 -0
  544. package/src/provider/models-snapshot.d.ts +0 -2
  545. package/src/provider/models-snapshot.js +0 -3
@@ -0,0 +1,109 @@
1
+ import { afterAll, afterEach, describe, expect } from "bun:test"
2
+ import { Effect, Layer } from "effect"
3
+ import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
4
+ import path from "path"
5
+ import { pathToFileURL } from "url"
6
+ import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
7
+ import { testEffect } from "../lib/effect"
8
+
9
+ const disableDefault = process.env.SAEEOL_DISABLE_DEFAULT_PLUGINS
10
+ process.env.SAEEOL_DISABLE_DEFAULT_PLUGINS = "1"
11
+
12
+ const { Flag } = await import("@saeeol/core/flag/flag")
13
+ const { Plugin } = await import("../../src/plugin/index")
14
+ const { Workspace } = await import("../../src/control-plane/workspace")
15
+ const { Instance } = await import("../../src/project/instance")
16
+ const it = testEffect(Layer.mergeAll(Plugin.defaultLayer, Workspace.defaultLayer, CrossSpawnSpawner.defaultLayer))
17
+
18
+ const experimental = Flag.SAEEOL_EXPERIMENTAL_WORKSPACES
19
+
20
+ Flag.SAEEOL_EXPERIMENTAL_WORKSPACES = true
21
+
22
+ afterEach(async () => {
23
+ await disposeAllInstances()
24
+ })
25
+
26
+ afterAll(() => {
27
+ if (disableDefault === undefined) {
28
+ delete process.env.SAEEOL_DISABLE_DEFAULT_PLUGINS
29
+ } else {
30
+ process.env.SAEEOL_DISABLE_DEFAULT_PLUGINS = disableDefault
31
+ }
32
+
33
+ Flag.SAEEOL_EXPERIMENTAL_WORKSPACES = experimental
34
+ })
35
+
36
+ describe("plugin.workspace", () => {
37
+ it.live("plugin can install a workspace adapter", () =>
38
+ provideTmpdirInstance((dir) =>
39
+ Effect.gen(function* () {
40
+ const type = `plug-${Math.random().toString(36).slice(2)}`
41
+ const file = path.join(dir, "plugin.ts")
42
+ const mark = path.join(dir, "created.json")
43
+ const space = path.join(dir, "space")
44
+ yield* Effect.promise(() =>
45
+ Bun.write(
46
+ file,
47
+ [
48
+ "export default async ({ experimental_workspace }) => {",
49
+ ` experimental_workspace.register(${JSON.stringify(type)}, {`,
50
+ ' name: "plug",',
51
+ ' description: "plugin workspace adapter",',
52
+ " configure(input) {",
53
+ ` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
54
+ " },",
55
+ " async create(input) {",
56
+ ` await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`,
57
+ " },",
58
+ " async remove() {},",
59
+ " target(input) {",
60
+ ' return { type: "local", directory: input.directory }',
61
+ " },",
62
+ " })",
63
+ " return {}",
64
+ "}",
65
+ "",
66
+ ].join("\n"),
67
+ ),
68
+ )
69
+
70
+ yield* Effect.promise(() =>
71
+ Bun.write(
72
+ path.join(dir, "saeeol.json"),
73
+ JSON.stringify(
74
+ {
75
+ $schema: "https://saeeol.ai/config.json",
76
+ plugin: [pathToFileURL(file).href],
77
+ },
78
+ null,
79
+ 2,
80
+ ),
81
+ ),
82
+ )
83
+
84
+ const plugin = yield* Plugin.Service
85
+ yield* plugin.init()
86
+ const workspace = yield* Workspace.Service
87
+ const info = yield* workspace.create({
88
+ type,
89
+ branch: null,
90
+ extra: { key: "value" },
91
+ projectID: Instance.project.id,
92
+ })
93
+
94
+ expect(info.type).toBe(type)
95
+ expect(info.name).toBe("plug")
96
+ expect(info.branch).toBe("plug/main")
97
+ expect(info.directory).toBe(space)
98
+ expect(info.extra).toEqual({ key: "value" })
99
+ expect(JSON.parse(yield* Effect.promise(() => Bun.file(mark).text()))).toMatchObject({
100
+ type,
101
+ name: "plug",
102
+ branch: "plug/main",
103
+ directory: space,
104
+ extra: { key: "value" },
105
+ })
106
+ }),
107
+ ),
108
+ )
109
+ })
@@ -0,0 +1,77 @@
1
+ // IMPORTANT: Set env vars BEFORE any imports from src/ directory
2
+ // xdg-basedir reads env vars at import time, so we must set these first
3
+ import os from "os"
4
+ import path from "path"
5
+ import fs from "fs/promises"
6
+ import { afterAll } from "bun:test"
7
+ import { remove as cleanup } from "./saeeol/cleanup"
8
+
9
+ // Set XDG env vars FIRST, before any src/ imports
10
+ const dir = path.join(os.tmpdir(), "saeeol-test-data-" + process.pid)
11
+ await fs.mkdir(dir, { recursive: true })
12
+ afterAll(async () => {
13
+ const { Database } = await import("../src/storage/db")
14
+ Database.close()
15
+ await cleanup(dir)
16
+ })
17
+
18
+ process.env["XDG_DATA_HOME"] = path.join(dir, "share")
19
+ process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
20
+ process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
21
+ process.env["XDG_STATE_HOME"] = path.join(dir, "state")
22
+ process.env["SAEEOL_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json")
23
+
24
+ // Set test home directory to isolate tests from user's actual home directory
25
+ // This prevents tests from picking up real user configs/skills from ~/.claude/skills
26
+ const testHome = path.join(dir, "home")
27
+ await fs.mkdir(testHome, { recursive: true })
28
+ process.env["SAEEOL_TEST_HOME"] = testHome
29
+
30
+ // Set test managed config directory to isolate tests from system managed settings
31
+ const testManagedConfigDir = path.join(dir, "managed")
32
+ process.env["SAEEOL_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir
33
+ process.env["SAEEOL_DISABLE_DEFAULT_PLUGINS"] = "true"
34
+
35
+ // Write the cache version file to prevent global/index.ts from clearing the cache
36
+ const cacheDir = path.join(dir, "cache", "saeeol")
37
+ await fs.mkdir(cacheDir, { recursive: true })
38
+ await fs.writeFile(path.join(cacheDir, "version"), "21")
39
+
40
+ // Clear provider and server auth env vars to ensure clean test state
41
+ delete process.env["ANTHROPIC_API_KEY"]
42
+ delete process.env["OPENAI_API_KEY"]
43
+ delete process.env["GOOGLE_API_KEY"]
44
+ delete process.env["GOOGLE_GENERATIVE_AI_API_KEY"]
45
+ delete process.env["AZURE_OPENAI_API_KEY"]
46
+ delete process.env["AWS_ACCESS_KEY_ID"]
47
+ delete process.env["AWS_PROFILE"]
48
+ delete process.env["AWS_REGION"]
49
+ delete process.env["AWS_BEARER_TOKEN_BEDROCK"]
50
+ delete process.env["OPENROUTER_API_KEY"]
51
+ delete process.env["LLM_GATEWAY_API_KEY"]
52
+ delete process.env["GROQ_API_KEY"]
53
+ delete process.env["MISTRAL_API_KEY"]
54
+ delete process.env["PERPLEXITY_API_KEY"]
55
+ delete process.env["TOGETHER_API_KEY"]
56
+ delete process.env["XAI_API_KEY"]
57
+ delete process.env["DEEPSEEK_API_KEY"]
58
+ delete process.env["FIREWORKS_API_KEY"]
59
+ delete process.env["CEREBRAS_API_KEY"]
60
+ delete process.env["SAMBANOVA_API_KEY"]
61
+ delete process.env["SAEEOL_SERVER_PASSWORD"]
62
+ delete process.env["SAEEOL_SERVER_USERNAME"]
63
+
64
+ // Use in-memory sqlite
65
+ process.env["SAEEOL_DB"] = ":memory:"
66
+
67
+ // Now safe to import from src/
68
+ const Log = await import("@saeeol/core/util/log")
69
+ const { initProjectors } = await import("../src/server/projectors")
70
+
71
+ void Log.init({
72
+ print: false,
73
+ dev: true,
74
+ level: "DEBUG",
75
+ })
76
+
77
+ initProjectors()
@@ -0,0 +1,276 @@
1
+ import { afterEach, describe, expect } from "bun:test"
2
+ import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
3
+ import { Effect, Fiber, Layer } from "effect"
4
+ import { InstanceRef } from "../../src/effect/instance-ref"
5
+ import { registerDisposer } from "../../src/effect/instance-registry"
6
+ import { Instance } from "../../src/project/instance"
7
+ import { InstanceStore } from "../../src/project/instance-store"
8
+ import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
9
+ import { testEffect } from "../lib/effect"
10
+
11
+ const it = testEffect(Layer.mergeAll(InstanceStore.defaultLayer, CrossSpawnSpawner.defaultLayer))
12
+
13
+ afterEach(async () => {
14
+ await disposeAllInstances()
15
+ })
16
+
17
+ describe("InstanceStore", () => {
18
+ it.live("loads instance context without installing ALS for the caller", () =>
19
+ Effect.gen(function* () {
20
+ const dir = yield* tmpdirScoped({ git: true })
21
+ const store = yield* InstanceStore.Service
22
+ const ctx = yield* store.load({ directory: dir })
23
+
24
+ expect(ctx.directory).toBe(dir)
25
+ expect(ctx.worktree).toBe(dir)
26
+ expect(() => Instance.current).toThrow()
27
+ }),
28
+ )
29
+
30
+ it.live("runs load init with InstanceRef provided", () =>
31
+ Effect.gen(function* () {
32
+ const dir = yield* tmpdirScoped({ git: true })
33
+ const store = yield* InstanceStore.Service
34
+ let initializedDirectory: string | undefined
35
+
36
+ yield* store.load({
37
+ directory: dir,
38
+ init: Effect.gen(function* () {
39
+ initializedDirectory = (yield* InstanceRef)?.directory
40
+ }),
41
+ })
42
+
43
+ expect(initializedDirectory).toBe(dir)
44
+ expect(() => Instance.current).toThrow()
45
+ }),
46
+ )
47
+
48
+ it.live("caches loaded instance context by directory", () =>
49
+ Effect.gen(function* () {
50
+ const dir = yield* tmpdirScoped({ git: true })
51
+ const store = yield* InstanceStore.Service
52
+ let initialized = 0
53
+
54
+ const first = yield* store.load({
55
+ directory: dir,
56
+ init: Effect.sync(() => {
57
+ initialized++
58
+ }),
59
+ })
60
+ const second = yield* store.load({
61
+ directory: dir,
62
+ init: Effect.sync(() => {
63
+ initialized++
64
+ }),
65
+ })
66
+
67
+ expect(second).toBe(first)
68
+ expect(initialized).toBe(1)
69
+ }),
70
+ )
71
+
72
+ it.live("dedupes concurrent loads while init is in flight", () =>
73
+ Effect.gen(function* () {
74
+ const dir = yield* tmpdirScoped({ git: true })
75
+ const store = yield* InstanceStore.Service
76
+ const started = Promise.withResolvers<void>()
77
+ const release = Promise.withResolvers<void>()
78
+ let initialized = 0
79
+
80
+ const first = yield* store
81
+ .load({
82
+ directory: dir,
83
+ init: Effect.promise(async () => {
84
+ initialized++
85
+ started.resolve()
86
+ await release.promise
87
+ }),
88
+ })
89
+ .pipe(Effect.forkScoped)
90
+
91
+ yield* Effect.promise(() => started.promise)
92
+
93
+ const second = yield* store
94
+ .load({
95
+ directory: dir,
96
+ init: Effect.sync(() => {
97
+ initialized++
98
+ }),
99
+ })
100
+ .pipe(Effect.forkScoped)
101
+
102
+ expect(initialized).toBe(1)
103
+ release.resolve()
104
+
105
+ const [firstCtx, secondCtx] = yield* Effect.all([Fiber.join(first), Fiber.join(second)])
106
+ expect(secondCtx).toBe(firstCtx)
107
+ expect(initialized).toBe(1)
108
+ }),
109
+ )
110
+
111
+ it.live("removes failed loads from the cache", () =>
112
+ Effect.gen(function* () {
113
+ const dir = yield* tmpdirScoped({ git: true })
114
+ const store = yield* InstanceStore.Service
115
+ let attempts = 0
116
+
117
+ const failed = yield* store
118
+ .load({
119
+ directory: dir,
120
+ init: Effect.sync(() => {
121
+ attempts++
122
+ throw new Error("init failed")
123
+ }),
124
+ })
125
+ .pipe(
126
+ Effect.as(false),
127
+ Effect.catchCause(() => Effect.succeed(true)),
128
+ )
129
+
130
+ expect(failed).toBe(true)
131
+
132
+ const ctx = yield* store.load({
133
+ directory: dir,
134
+ init: Effect.sync(() => {
135
+ attempts++
136
+ }),
137
+ })
138
+
139
+ expect(ctx.directory).toBe(dir)
140
+ expect(attempts).toBe(2)
141
+ }),
142
+ )
143
+
144
+ it.live("reload replaces the cached context", () =>
145
+ Effect.gen(function* () {
146
+ const dir = yield* tmpdirScoped({ git: true })
147
+ const store = yield* InstanceStore.Service
148
+
149
+ const first = yield* store.load({ directory: dir })
150
+ const second = yield* store.reload({ directory: dir })
151
+ const cached = yield* store.load({ directory: dir })
152
+
153
+ expect(second).not.toBe(first)
154
+ expect(cached).toBe(second)
155
+ }),
156
+ )
157
+
158
+ it.live("stale dispose does not delete an in-flight reload", () =>
159
+ Effect.gen(function* () {
160
+ const dir = yield* tmpdirScoped({ git: true })
161
+ const store = yield* InstanceStore.Service
162
+ const reloading = Promise.withResolvers<void>()
163
+ const releaseReload = Promise.withResolvers<void>()
164
+ const disposed: Array<string> = []
165
+ const off = registerDisposer(async (directory) => {
166
+ disposed.push(directory)
167
+ })
168
+ yield* Effect.addFinalizer(() => Effect.sync(off))
169
+
170
+ const first = yield* store.load({ directory: dir })
171
+ const reload = yield* store
172
+ .reload({
173
+ directory: dir,
174
+ init: Effect.promise(async () => {
175
+ reloading.resolve()
176
+ await releaseReload.promise
177
+ }),
178
+ })
179
+ .pipe(Effect.forkScoped)
180
+
181
+ yield* Effect.promise(() => reloading.promise)
182
+ const staleDispose = yield* store.dispose(first).pipe(Effect.forkScoped)
183
+ releaseReload.resolve()
184
+
185
+ const second = yield* Fiber.join(reload)
186
+ yield* Fiber.join(staleDispose)
187
+
188
+ expect(disposed).toEqual([dir])
189
+ expect(yield* store.load({ directory: dir })).toBe(second)
190
+ }),
191
+ )
192
+
193
+ it.live("dedupes concurrent disposeAll calls", () =>
194
+ Effect.gen(function* () {
195
+ const dir = yield* tmpdirScoped({ git: true })
196
+ const store = yield* InstanceStore.Service
197
+ const disposing = Promise.withResolvers<void>()
198
+ const releaseDispose = Promise.withResolvers<void>()
199
+ const disposed: Array<string> = []
200
+ const off = registerDisposer(async (directory) => {
201
+ disposed.push(directory)
202
+ disposing.resolve()
203
+ await releaseDispose.promise
204
+ })
205
+ yield* Effect.addFinalizer(() => Effect.sync(off))
206
+
207
+ yield* store.load({ directory: dir })
208
+ const first = yield* store.disposeAll().pipe(Effect.forkScoped)
209
+ yield* Effect.promise(() => disposing.promise)
210
+ const second = yield* store.disposeAll().pipe(Effect.forkScoped)
211
+
212
+ expect(disposed).toEqual([dir])
213
+ releaseDispose.resolve()
214
+ yield* Effect.all([Fiber.join(first), Fiber.join(second)])
215
+ expect(disposed).toEqual([dir])
216
+ }),
217
+ )
218
+
219
+ it.live("re-arms disposeAll after completion", () =>
220
+ Effect.gen(function* () {
221
+ const dir1 = yield* tmpdirScoped({ git: true })
222
+ const dir2 = yield* tmpdirScoped({ git: true })
223
+ const store = yield* InstanceStore.Service
224
+ const disposed: Array<string> = []
225
+ const off = registerDisposer(async (directory) => {
226
+ disposed.push(directory)
227
+ })
228
+ yield* Effect.addFinalizer(() => Effect.sync(off))
229
+
230
+ yield* store.load({ directory: dir1 })
231
+ yield* store.disposeAll()
232
+ expect(disposed).toEqual([dir1])
233
+
234
+ yield* store.load({ directory: dir2 })
235
+ yield* store.disposeAll()
236
+ expect(disposed).toEqual([dir1, dir2])
237
+ }),
238
+ )
239
+
240
+ it.live("keeps Instance.provide as the legacy ALS wrapper", () =>
241
+ Effect.gen(function* () {
242
+ const dir = yield* tmpdirScoped({ git: true })
243
+
244
+ const directory = yield* Effect.promise(() =>
245
+ Instance.provide({
246
+ directory: dir,
247
+ fn: () => Instance.directory,
248
+ }),
249
+ )
250
+
251
+ expect(directory).toBe(dir)
252
+ expect(() => Instance.current).toThrow()
253
+ }),
254
+ )
255
+ // SaeeolIndexing.init that it forkDetaches) can read Instance.directory. Upstream's test
256
+ // asserted the opposite contract; rewrite to assert Saeeol's contract.
257
+ it.live("installs Instance ALS around Effect init for Saeeol bootstrap compatibility", () =>
258
+ Effect.gen(function* () {
259
+ const dir = yield* tmpdirScoped()
260
+ let directoryDuringInit: string | undefined
261
+
262
+ const directory = yield* Effect.promise(() =>
263
+ Instance.provide({
264
+ directory: dir,
265
+ init: Effect.sync(() => {
266
+ directoryDuringInit = Instance.directory
267
+ }),
268
+ fn: () => Instance.directory,
269
+ }),
270
+ )
271
+
272
+ expect(directoryDuringInit).toBe(dir)
273
+ expect(directory).toBe(dir)
274
+ }),
275
+ )
276
+ })
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Project } from "@/project/project"
3
+ import { Database } from "@/storage/db"
4
+ import { eq } from "drizzle-orm"
5
+ import { SessionTable } from "../../src/session/session.sql"
6
+ import { ProjectTable } from "../../src/project/project.sql"
7
+ import { ProjectID } from "../../src/project/schema"
8
+ import { SessionID } from "../../src/session/schema"
9
+ import * as Log from "@saeeol/core/util/log"
10
+ import { $ } from "bun"
11
+ import { tmpdir } from "../fixture/fixture"
12
+ import { Effect } from "effect"
13
+
14
+ Log.init({ print: false })
15
+
16
+ function run<A>(fn: (svc: Project.Interface) => Effect.Effect<A>) {
17
+ return Effect.runPromise(
18
+ Effect.gen(function* () {
19
+ const svc = yield* Project.Service
20
+ return yield* fn(svc)
21
+ }).pipe(Effect.provide(Project.defaultLayer)),
22
+ )
23
+ }
24
+
25
+ function uid() {
26
+ return SessionID.make(crypto.randomUUID())
27
+ }
28
+
29
+ function seed(opts: { id: SessionID; dir: string; project: ProjectID }) {
30
+ const now = Date.now()
31
+ Database.use((db) =>
32
+ db
33
+ .insert(SessionTable)
34
+ .values({
35
+ id: opts.id,
36
+ project_id: opts.project,
37
+ slug: opts.id,
38
+ directory: opts.dir,
39
+ title: "test",
40
+ version: "0.0.0-test",
41
+ time_created: now,
42
+ time_updated: now,
43
+ })
44
+ .run(),
45
+ )
46
+ }
47
+
48
+ function ensureGlobal() {
49
+ Database.use((db) =>
50
+ db
51
+ .insert(ProjectTable)
52
+ .values({
53
+ id: ProjectID.global,
54
+ worktree: "/",
55
+ time_created: Date.now(),
56
+ time_updated: Date.now(),
57
+ sandboxes: [],
58
+ })
59
+ .onConflictDoNothing()
60
+ .run(),
61
+ )
62
+ }
63
+
64
+ describe("migrateFromGlobal", () => {
65
+ test("migrates global sessions on first project creation", async () => {
66
+ // 1. Start with git init but no commits — creates "global" project row
67
+ await using tmp = await tmpdir()
68
+ await $`git init`.cwd(tmp.path).quiet()
69
+ await $`git config user.name "Test"`.cwd(tmp.path).quiet()
70
+ await $`git config user.email "test@saeeol.test"`.cwd(tmp.path).quiet()
71
+ await $`git config commit.gpgsign false`.cwd(tmp.path).quiet()
72
+ const { project: pre } = await run((svc) => svc.fromDirectory(tmp.path))
73
+ expect(pre.id).toBe(ProjectID.global)
74
+
75
+ // 2. Seed a session under "global" with matching directory
76
+ const id = uid()
77
+ seed({ id, dir: tmp.path, project: ProjectID.global })
78
+
79
+ // 3. Make a commit so the project gets a real ID
80
+ await $`git commit --allow-empty -m "root"`.cwd(tmp.path).quiet()
81
+
82
+ const { project: real } = await run((svc) => svc.fromDirectory(tmp.path))
83
+ expect(real.id).not.toBe(ProjectID.global)
84
+
85
+ // 4. The session should have been migrated to the real project ID
86
+ const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
87
+ expect(row).toBeDefined()
88
+ expect(row!.project_id).toBe(real.id)
89
+ })
90
+
91
+ test("migrates global sessions even when project row already exists", async () => {
92
+ // 1. Create a repo with a commit — real project ID created immediately
93
+ await using tmp = await tmpdir({ git: true })
94
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
95
+ expect(project.id).not.toBe(ProjectID.global)
96
+
97
+ // 2. Ensure "global" project row exists (as it would from a prior no-git session)
98
+ ensureGlobal()
99
+
100
+ // 3. Seed a session under "global" with matching directory.
101
+ // This simulates a session created before git init that wasn't
102
+ // present when the real project row was first created.
103
+ const id = uid()
104
+ seed({ id, dir: tmp.path, project: ProjectID.global })
105
+
106
+ // 4. Call fromDirectory again — project row already exists,
107
+ // so the current code skips migration entirely. This is the bug.
108
+ await run((svc) => svc.fromDirectory(tmp.path))
109
+
110
+ const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
111
+ expect(row).toBeDefined()
112
+ expect(row!.project_id).toBe(project.id)
113
+ })
114
+
115
+ test("does not claim sessions with empty directory", async () => {
116
+ await using tmp = await tmpdir({ git: true })
117
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
118
+ expect(project.id).not.toBe(ProjectID.global)
119
+
120
+ ensureGlobal()
121
+
122
+ // Legacy sessions may lack a directory value.
123
+ // Without a matching origin directory, they should remain global.
124
+ const id = uid()
125
+ seed({ id, dir: "", project: ProjectID.global })
126
+
127
+ await run((svc) => svc.fromDirectory(tmp.path))
128
+
129
+ const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
130
+ expect(row).toBeDefined()
131
+ expect(row!.project_id).toBe(ProjectID.global)
132
+ })
133
+
134
+ test("does not steal sessions from unrelated directories", async () => {
135
+ await using tmp = await tmpdir({ git: true })
136
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
137
+ expect(project.id).not.toBe(ProjectID.global)
138
+
139
+ ensureGlobal()
140
+
141
+ // Seed a session under "global" but for a DIFFERENT directory
142
+ const id = uid()
143
+ seed({ id, dir: "/some/other/dir", project: ProjectID.global })
144
+
145
+ await run((svc) => svc.fromDirectory(tmp.path))
146
+
147
+ const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
148
+ expect(row).toBeDefined()
149
+ // Should remain under "global" — not stolen
150
+ expect(row!.project_id).toBe(ProjectID.global)
151
+ })
152
+ })