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,600 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Project } from "@/project/project"
3
+ import * as Log from "@saeeol/core/util/log"
4
+ import { $ } from "bun"
5
+ import path from "path"
6
+ import { tmpdir } from "../fixture/fixture"
7
+ import { GlobalBus } from "../../src/bus/global"
8
+ import { ProjectID } from "../../src/project/schema"
9
+ import { Effect, Layer, Stream } from "effect"
10
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
11
+ import { NodePath } from "@effect/platform-node"
12
+ import { AppFileSystem } from "@saeeol/core/filesystem"
13
+ import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
14
+
15
+ void Log.init({ print: false })
16
+
17
+ const encoder = new TextEncoder()
18
+
19
+ function run<A>(fn: (svc: Project.Interface) => Effect.Effect<A>, layer = Project.defaultLayer) {
20
+ return Effect.runPromise(
21
+ Effect.gen(function* () {
22
+ const svc = yield* Project.Service
23
+ return yield* fn(svc)
24
+ }).pipe(Effect.provide(layer)),
25
+ )
26
+ }
27
+
28
+ /**
29
+ * Creates a mock ChildProcessSpawner layer that intercepts git subcommands
30
+ * matching `failArg` and returns exit code 128, while delegating everything
31
+ * else to the real CrossSpawnSpawner.
32
+ */
33
+ function mockGitFailure(failArg: string) {
34
+ return Layer.effect(
35
+ ChildProcessSpawner.ChildProcessSpawner,
36
+ Effect.gen(function* () {
37
+ const real = yield* ChildProcessSpawner.ChildProcessSpawner
38
+ return ChildProcessSpawner.make(
39
+ Effect.fnUntraced(function* (command) {
40
+ const std = ChildProcess.isStandardCommand(command) ? command : undefined
41
+ if (std?.command === "git" && std.args.some((a) => a === failArg)) {
42
+ return ChildProcessSpawner.makeHandle({
43
+ pid: ChildProcessSpawner.ProcessId(0),
44
+ exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(128)),
45
+ isRunning: Effect.succeed(false),
46
+ kill: () => Effect.void,
47
+ stdin: { [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") } as any,
48
+ stdout: Stream.empty,
49
+ stderr: Stream.make(encoder.encode("fatal: simulated failure\n")),
50
+ all: Stream.empty,
51
+ getInputFd: () => ({ [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") }) as any,
52
+ getOutputFd: () => Stream.empty,
53
+ unref: Effect.succeed(Effect.void),
54
+ })
55
+ }
56
+ return yield* real.spawn(command)
57
+ }),
58
+ )
59
+ }),
60
+ ).pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
61
+ }
62
+
63
+ function projectLayerWithFailure(failArg: string) {
64
+ return Project.layer.pipe(
65
+ Layer.provide(mockGitFailure(failArg)),
66
+ Layer.provide(AppFileSystem.defaultLayer),
67
+ Layer.provide(NodePath.layer),
68
+ )
69
+ }
70
+
71
+ describe("Project.fromDirectory", () => {
72
+ test("should handle git repository with no commits", async () => {
73
+ await using tmp = await tmpdir()
74
+ await $`git init`.cwd(tmp.path).quiet()
75
+
76
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
77
+
78
+ expect(project).toBeDefined()
79
+ expect(project.id).toBe(ProjectID.global)
80
+ expect(project.vcs).toBe("git")
81
+ expect(project.worktree).toBe(tmp.path)
82
+
83
+ const saeeolFile = path.join(tmp.path, ".git", "saeeol")
84
+ expect(await Bun.file(saeeolFile).exists()).toBe(false)
85
+ })
86
+
87
+ test("should handle git repository with commits", async () => {
88
+ await using tmp = await tmpdir({ git: true })
89
+
90
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
91
+
92
+ expect(project).toBeDefined()
93
+ expect(project.id).not.toBe(ProjectID.global)
94
+ expect(project.vcs).toBe("git")
95
+ expect(project.worktree).toBe(tmp.path)
96
+
97
+ const saeeolFile = path.join(tmp.path, ".git", "saeeol")
98
+ expect(await Bun.file(saeeolFile).exists()).toBe(true)
99
+ })
100
+
101
+ test("returns global for non-git directory", async () => {
102
+ await using tmp = await tmpdir()
103
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
104
+ expect(project.id).toBe(ProjectID.global)
105
+ })
106
+
107
+ test("derives stable project ID from root commit", async () => {
108
+ await using tmp = await tmpdir({ git: true })
109
+ const { project: a } = await run((svc) => svc.fromDirectory(tmp.path))
110
+ const { project: b } = await run((svc) => svc.fromDirectory(tmp.path))
111
+ expect(b.id).toBe(a.id)
112
+ })
113
+ })
114
+
115
+ describe("Project.fromDirectory git failure paths", () => {
116
+ test("keeps vcs when rev-list exits non-zero (no commits)", async () => {
117
+ await using tmp = await tmpdir()
118
+ await $`git init`.cwd(tmp.path).quiet()
119
+
120
+ // rev-list fails because HEAD doesn't exist yet — this is the natural scenario
121
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
122
+ expect(project.vcs).toBe("git")
123
+ expect(project.id).toBe(ProjectID.global)
124
+ expect(project.worktree).toBe(tmp.path)
125
+ })
126
+
127
+ test("handles show-toplevel failure gracefully", async () => {
128
+ await using tmp = await tmpdir({ git: true })
129
+ const layer = projectLayerWithFailure("--show-toplevel")
130
+
131
+ const { project, sandbox } = await run((svc) => svc.fromDirectory(tmp.path), layer)
132
+ expect(project.worktree).toBe(tmp.path)
133
+ expect(sandbox).toBe(tmp.path)
134
+ })
135
+
136
+ test("handles git-common-dir failure gracefully", async () => {
137
+ await using tmp = await tmpdir({ git: true })
138
+ const layer = projectLayerWithFailure("--git-common-dir")
139
+
140
+ const { project, sandbox } = await run((svc) => svc.fromDirectory(tmp.path), layer)
141
+ expect(project.worktree).toBe(tmp.path)
142
+ expect(sandbox).toBe(tmp.path)
143
+ })
144
+ })
145
+
146
+ describe("Project.fromDirectory with worktrees", () => {
147
+ test("should set worktree to root when called from root", async () => {
148
+ await using tmp = await tmpdir({ git: true })
149
+
150
+ const { project, sandbox } = await run((svc) => svc.fromDirectory(tmp.path))
151
+
152
+ expect(project.worktree).toBe(tmp.path)
153
+ expect(sandbox).toBe(tmp.path)
154
+ expect(project.sandboxes).not.toContain(tmp.path)
155
+ })
156
+
157
+ test("should set worktree to root when called from a worktree", async () => {
158
+ await using tmp = await tmpdir({ git: true })
159
+
160
+ const worktreePath = path.join(tmp.path, "..", path.basename(tmp.path) + "-worktree")
161
+ try {
162
+ await $`git worktree add ${worktreePath} -b test-branch-${Date.now()}`.cwd(tmp.path).quiet()
163
+
164
+ const { project, sandbox } = await run((svc) => svc.fromDirectory(worktreePath))
165
+
166
+ expect(project.worktree).toBe(tmp.path)
167
+ expect(sandbox).toBe(worktreePath)
168
+ expect(project.sandboxes).toContain(worktreePath)
169
+ expect(project.sandboxes).not.toContain(tmp.path)
170
+ } finally {
171
+ await $`git worktree remove ${worktreePath}`
172
+ .cwd(tmp.path)
173
+ .quiet()
174
+ .catch(() => {})
175
+ }
176
+ })
177
+
178
+ test("worktree should share project ID with main repo", async () => {
179
+ await using tmp = await tmpdir({ git: true })
180
+
181
+ const { project: main } = await run((svc) => svc.fromDirectory(tmp.path))
182
+
183
+ const worktreePath = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt-shared")
184
+ try {
185
+ await $`git worktree add ${worktreePath} -b shared-${Date.now()}`.cwd(tmp.path).quiet()
186
+
187
+ const { project: wt } = await run((svc) => svc.fromDirectory(worktreePath))
188
+
189
+ expect(wt.id).toBe(main.id)
190
+
191
+ // Cache should live in the common .git dir, not the worktree's .git file
192
+ const cache = path.join(tmp.path, ".git", "saeeol")
193
+ const exists = await Bun.file(cache).exists()
194
+ expect(exists).toBe(true)
195
+ } finally {
196
+ await $`git worktree remove ${worktreePath}`
197
+ .cwd(tmp.path)
198
+ .quiet()
199
+ .catch(() => {})
200
+ }
201
+ })
202
+
203
+ test("separate clones of the same repo should share project ID", async () => {
204
+ await using tmp = await tmpdir({ git: true })
205
+
206
+ // Create a bare remote, push, then clone into a second directory
207
+ const bare = tmp.path + "-bare"
208
+ const clone = tmp.path + "-clone"
209
+ try {
210
+ await $`git clone --bare ${tmp.path} ${bare}`.quiet()
211
+ await $`git clone ${bare} ${clone}`.quiet()
212
+
213
+ const { project: a } = await run((svc) => svc.fromDirectory(tmp.path))
214
+ const { project: b } = await run((svc) => svc.fromDirectory(clone))
215
+
216
+ expect(b.id).toBe(a.id)
217
+ } finally {
218
+ await $`rm -rf ${bare} ${clone}`.quiet().nothrow()
219
+ }
220
+ })
221
+
222
+ test("should accumulate multiple worktrees in sandboxes", async () => {
223
+ await using tmp = await tmpdir({ git: true })
224
+
225
+ const worktree1 = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt1")
226
+ const worktree2 = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt2")
227
+ try {
228
+ await $`git worktree add ${worktree1} -b branch-${Date.now()}`.cwd(tmp.path).quiet()
229
+ await $`git worktree add ${worktree2} -b branch-${Date.now() + 1}`.cwd(tmp.path).quiet()
230
+
231
+ await run((svc) => svc.fromDirectory(worktree1))
232
+ const { project } = await run((svc) => svc.fromDirectory(worktree2))
233
+
234
+ expect(project.worktree).toBe(tmp.path)
235
+ expect(project.sandboxes).toContain(worktree1)
236
+ expect(project.sandboxes).toContain(worktree2)
237
+ expect(project.sandboxes).not.toContain(tmp.path)
238
+ } finally {
239
+ await $`git worktree remove ${worktree1}`
240
+ .cwd(tmp.path)
241
+ .quiet()
242
+ .catch(() => {})
243
+ await $`git worktree remove ${worktree2}`
244
+ .cwd(tmp.path)
245
+ .quiet()
246
+ .catch(() => {})
247
+ }
248
+ })
249
+ })
250
+
251
+ describe("Project.discover", () => {
252
+ test("should discover favicon.png in root", async () => {
253
+ await using tmp = await tmpdir({ git: true })
254
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
255
+
256
+ const pngData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
257
+ await Bun.write(path.join(tmp.path, "favicon.png"), pngData)
258
+
259
+ await run((svc) => svc.discover(project))
260
+
261
+ const updated = Project.get(project.id)
262
+ expect(updated).toBeDefined()
263
+ expect(updated!.icon).toBeDefined()
264
+ expect(updated!.icon?.url).toStartWith("data:")
265
+ expect(updated!.icon?.url).toContain("base64")
266
+ expect(updated!.icon?.color).toBeUndefined()
267
+ })
268
+
269
+ test("should not discover non-image files", async () => {
270
+ await using tmp = await tmpdir({ git: true })
271
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
272
+
273
+ await Bun.write(path.join(tmp.path, "favicon.txt"), "not an image")
274
+
275
+ await run((svc) => svc.discover(project))
276
+
277
+ const updated = Project.get(project.id)
278
+ expect(updated).toBeDefined()
279
+ expect(updated!.icon).toBeUndefined()
280
+ })
281
+
282
+ test("should not discover favicon when override is set", async () => {
283
+ await using tmp = await tmpdir({ git: true })
284
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
285
+
286
+ await run((svc) =>
287
+ svc.update({
288
+ projectID: project.id,
289
+ icon: { override: "data:image/png;base64,override" },
290
+ }),
291
+ )
292
+
293
+ const updatedProject = await run((svc) => svc.get(project.id))
294
+ if (!updatedProject) throw new Error("Project not found")
295
+
296
+ const pngData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
297
+ await Bun.write(path.join(tmp.path, "favicon.png"), pngData)
298
+
299
+ await run((svc) => svc.discover(updatedProject))
300
+
301
+ const updated = Project.get(project.id)
302
+ expect(updated).toBeDefined()
303
+ expect(updated!.icon?.override).toBe("data:image/png;base64,override")
304
+ expect(updated!.icon?.url).toBeUndefined()
305
+ })
306
+ })
307
+
308
+ describe("Project.update", () => {
309
+ test("should update name", async () => {
310
+ await using tmp = await tmpdir({ git: true })
311
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
312
+
313
+ const updated = await run((svc) =>
314
+ svc.update({
315
+ projectID: project.id,
316
+ name: "New Project Name",
317
+ }),
318
+ )
319
+
320
+ expect(updated.name).toBe("New Project Name")
321
+
322
+ const fromDb = Project.get(project.id)
323
+ expect(fromDb?.name).toBe("New Project Name")
324
+ })
325
+
326
+ test("should update icon url", async () => {
327
+ await using tmp = await tmpdir({ git: true })
328
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
329
+
330
+ const updated = await run((svc) =>
331
+ svc.update({
332
+ projectID: project.id,
333
+ icon: { url: "https://example.com/icon.png" },
334
+ }),
335
+ )
336
+
337
+ expect(updated.icon?.url).toBe("https://example.com/icon.png")
338
+
339
+ const fromDb = Project.get(project.id)
340
+ expect(fromDb?.icon?.url).toBe("https://example.com/icon.png")
341
+ })
342
+
343
+ test("should update icon color", async () => {
344
+ await using tmp = await tmpdir({ git: true })
345
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
346
+
347
+ const updated = await run((svc) =>
348
+ svc.update({
349
+ projectID: project.id,
350
+ icon: { color: "#ff0000" },
351
+ }),
352
+ )
353
+
354
+ expect(updated.icon?.color).toBe("#ff0000")
355
+
356
+ const fromDb = Project.get(project.id)
357
+ expect(fromDb?.icon?.color).toBe("#ff0000")
358
+ })
359
+
360
+ test("should update icon override", async () => {
361
+ await using tmp = await tmpdir({ git: true })
362
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
363
+
364
+ const updated = await run((svc) =>
365
+ svc.update({
366
+ projectID: project.id,
367
+ icon: { override: "data:image/png;base64,abc123" },
368
+ }),
369
+ )
370
+
371
+ expect(updated.icon?.override).toBe("data:image/png;base64,abc123")
372
+
373
+ const fromDb = Project.get(project.id)
374
+ expect(fromDb?.icon?.override).toBe("data:image/png;base64,abc123")
375
+ })
376
+
377
+ test("should update commands", async () => {
378
+ await using tmp = await tmpdir({ git: true })
379
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
380
+
381
+ const updated = await run((svc) =>
382
+ svc.update({
383
+ projectID: project.id,
384
+ commands: { start: "npm run dev" },
385
+ }),
386
+ )
387
+
388
+ expect(updated.commands?.start).toBe("npm run dev")
389
+
390
+ const fromDb = Project.get(project.id)
391
+ expect(fromDb?.commands?.start).toBe("npm run dev")
392
+ })
393
+
394
+ test("should throw error when project not found", async () => {
395
+ await expect(
396
+ run((svc) =>
397
+ svc.update({
398
+ projectID: ProjectID.make("nonexistent-project-id"),
399
+ name: "Should Fail",
400
+ }),
401
+ ),
402
+ ).rejects.toThrow("Project not found: nonexistent-project-id")
403
+ })
404
+
405
+ test("should emit GlobalBus event on update", async () => {
406
+ await using tmp = await tmpdir({ git: true })
407
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
408
+
409
+ let eventPayload: any = null
410
+ const on = (data: any) => {
411
+ eventPayload = data
412
+ }
413
+ GlobalBus.on("event", on)
414
+
415
+ try {
416
+ await run((svc) => svc.update({ projectID: project.id, name: "Updated Name" }))
417
+
418
+ expect(eventPayload).not.toBeNull()
419
+ expect(eventPayload.payload.type).toBe("project.updated")
420
+ expect(eventPayload.payload.properties.name).toBe("Updated Name")
421
+ } finally {
422
+ GlobalBus.off("event", on)
423
+ }
424
+ })
425
+
426
+ test("should update multiple fields at once", async () => {
427
+ await using tmp = await tmpdir({ git: true })
428
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
429
+
430
+ const updated = await run((svc) =>
431
+ svc.update({
432
+ projectID: project.id,
433
+ name: "Multi Update",
434
+ icon: { url: "https://example.com/favicon.ico", override: "data:image/png;base64,abc123", color: "#00ff00" },
435
+ commands: { start: "make start" },
436
+ }),
437
+ )
438
+
439
+ expect(updated.name).toBe("Multi Update")
440
+ expect(updated.icon?.url).toBe("https://example.com/favicon.ico")
441
+ expect(updated.icon?.override).toBe("data:image/png;base64,abc123")
442
+ expect(updated.icon?.color).toBe("#00ff00")
443
+ expect(updated.commands?.start).toBe("make start")
444
+ })
445
+ })
446
+
447
+ describe("Project.list and Project.get", () => {
448
+ test("list returns all projects", async () => {
449
+ await using tmp = await tmpdir({ git: true })
450
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
451
+
452
+ const all = Project.list()
453
+ expect(all.length).toBeGreaterThan(0)
454
+ expect(all.find((p) => p.id === project.id)).toBeDefined()
455
+ })
456
+
457
+ test("get returns project by id", async () => {
458
+ await using tmp = await tmpdir({ git: true })
459
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
460
+
461
+ const found = Project.get(project.id)
462
+ expect(found).toBeDefined()
463
+ expect(found!.id).toBe(project.id)
464
+ })
465
+
466
+ test("get returns undefined for unknown id", () => {
467
+ const found = Project.get(ProjectID.make("nonexistent"))
468
+ expect(found).toBeUndefined()
469
+ })
470
+ })
471
+
472
+ describe("Project.setInitialized", () => {
473
+ test("sets time_initialized on project", async () => {
474
+ await using tmp = await tmpdir({ git: true })
475
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
476
+
477
+ expect(project.time.initialized).toBeUndefined()
478
+
479
+ Project.setInitialized(project.id)
480
+
481
+ const updated = Project.get(project.id)
482
+ expect(updated?.time.initialized).toBeDefined()
483
+ })
484
+ })
485
+
486
+ describe("Project.addSandbox and Project.removeSandbox", () => {
487
+ test("addSandbox adds directory and removeSandbox removes it", async () => {
488
+ await using tmp = await tmpdir({ git: true })
489
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
490
+ const sandboxDir = path.join(tmp.path, "sandbox-test")
491
+
492
+ await run((svc) => svc.addSandbox(project.id, sandboxDir))
493
+
494
+ let found = Project.get(project.id)
495
+ expect(found?.sandboxes).toContain(sandboxDir)
496
+
497
+ await run((svc) => svc.removeSandbox(project.id, sandboxDir))
498
+
499
+ found = Project.get(project.id)
500
+ expect(found?.sandboxes).not.toContain(sandboxDir)
501
+ })
502
+
503
+ test("addSandbox emits GlobalBus event", async () => {
504
+ await using tmp = await tmpdir({ git: true })
505
+ const { project } = await run((svc) => svc.fromDirectory(tmp.path))
506
+ const sandboxDir = path.join(tmp.path, "sandbox-event")
507
+
508
+ const events: any[] = []
509
+ const on = (evt: any) => events.push(evt)
510
+ GlobalBus.on("event", on)
511
+
512
+ await run((svc) => svc.addSandbox(project.id, sandboxDir))
513
+
514
+ GlobalBus.off("event", on)
515
+ expect(events.some((e) => e.payload.type === Project.Event.Updated.type)).toBe(true)
516
+ })
517
+ })
518
+
519
+ describe("Project.fromDirectory with bare repos", () => {
520
+ test("worktree from bare repo should cache in bare repo, not parent", async () => {
521
+ await using tmp = await tmpdir({ git: true })
522
+
523
+ const parentDir = path.dirname(tmp.path)
524
+ const barePath = path.join(parentDir, `bare-${Date.now()}.git`)
525
+ const worktreePath = path.join(parentDir, `worktree-${Date.now()}`)
526
+
527
+ try {
528
+ await $`git clone --bare ${tmp.path} ${barePath}`.quiet()
529
+ await $`git worktree add ${worktreePath} HEAD`.cwd(barePath).quiet()
530
+
531
+ const { project } = await run((svc) => svc.fromDirectory(worktreePath))
532
+
533
+ expect(project.id).not.toBe(ProjectID.global)
534
+ expect(project.worktree).toBe(barePath)
535
+
536
+ const correctCache = path.join(barePath, "saeeol")
537
+ const wrongCache = path.join(parentDir, ".git", "saeeol")
538
+
539
+ expect(await Bun.file(correctCache).exists()).toBe(true)
540
+ expect(await Bun.file(wrongCache).exists()).toBe(false)
541
+ } finally {
542
+ await $`rm -rf ${barePath} ${worktreePath}`.quiet().nothrow()
543
+ }
544
+ })
545
+
546
+ test("different bare repos under same parent should not share project ID", async () => {
547
+ await using tmp1 = await tmpdir({ git: true })
548
+ await using tmp2 = await tmpdir({ git: true })
549
+
550
+ const parentDir = path.dirname(tmp1.path)
551
+ const bareA = path.join(parentDir, `bare-a-${Date.now()}.git`)
552
+ const bareB = path.join(parentDir, `bare-b-${Date.now()}.git`)
553
+ const worktreeA = path.join(parentDir, `wt-a-${Date.now()}`)
554
+ const worktreeB = path.join(parentDir, `wt-b-${Date.now()}`)
555
+
556
+ try {
557
+ await $`git clone --bare ${tmp1.path} ${bareA}`.quiet()
558
+ await $`git clone --bare ${tmp2.path} ${bareB}`.quiet()
559
+ await $`git worktree add ${worktreeA} HEAD`.cwd(bareA).quiet()
560
+ await $`git worktree add ${worktreeB} HEAD`.cwd(bareB).quiet()
561
+
562
+ const { project: projA } = await run((svc) => svc.fromDirectory(worktreeA))
563
+ const { project: projB } = await run((svc) => svc.fromDirectory(worktreeB))
564
+
565
+ expect(projA.id).not.toBe(projB.id)
566
+ const cacheA = path.join(bareA, "saeeol")
567
+ const cacheB = path.join(bareB, "saeeol")
568
+ const wrongCache = path.join(parentDir, ".git", "saeeol")
569
+
570
+ expect(await Bun.file(cacheA).exists()).toBe(true)
571
+ expect(await Bun.file(cacheB).exists()).toBe(true)
572
+ expect(await Bun.file(wrongCache).exists()).toBe(false)
573
+ } finally {
574
+ await $`rm -rf ${bareA} ${bareB} ${worktreeA} ${worktreeB}`.quiet().nothrow()
575
+ }
576
+ })
577
+
578
+ test("bare repo without .git suffix is still detected via core.bare", async () => {
579
+ await using tmp = await tmpdir({ git: true })
580
+
581
+ const parentDir = path.dirname(tmp.path)
582
+ const barePath = path.join(parentDir, `bare-no-suffix-${Date.now()}`)
583
+ const worktreePath = path.join(parentDir, `worktree-${Date.now()}`)
584
+
585
+ try {
586
+ await $`git clone --bare ${tmp.path} ${barePath}`.quiet()
587
+ await $`git worktree add ${worktreePath} HEAD`.cwd(barePath).quiet()
588
+
589
+ const { project } = await run((svc) => svc.fromDirectory(worktreePath))
590
+
591
+ expect(project.id).not.toBe(ProjectID.global)
592
+ expect(project.worktree).toBe(barePath)
593
+
594
+ const correctCache = path.join(barePath, "saeeol")
595
+ expect(await Bun.file(correctCache).exists()).toBe(true)
596
+ } finally {
597
+ await $`rm -rf ${barePath} ${worktreePath}`.quiet().nothrow()
598
+ }
599
+ })
600
+ })