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,96 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import path from "path"
4
+ import type { Permission } from "../../src/permission"
5
+ import { Instance } from "../../src/project/instance"
6
+ import { InstanceStore } from "../../src/project/instance-store"
7
+ import { SessionID, MessageID } from "../../src/session/schema"
8
+ import { assertExternalDirectory } from "../../src/tool/external-directory"
9
+ import type { Tool } from "../../src/tool/tool"
10
+ import { Filesystem } from "../../src/util/filesystem"
11
+ import { AppFileSystem } from "@saeeol/core/filesystem"
12
+ import { tmpdir } from "../fixture/fixture"
13
+
14
+ const base: Omit<Tool.Context, "ask"> = {
15
+ sessionID: SessionID.make("ses_test-boundary-session"),
16
+ messageID: MessageID.make(""),
17
+ callID: "",
18
+ agent: "code",
19
+ abort: AbortSignal.any([]),
20
+ messages: [],
21
+ metadata: () => Effect.void,
22
+ }
23
+
24
+ const glob = (p: string) =>
25
+ process.platform === "win32" ? AppFileSystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
26
+
27
+ const asks = () => {
28
+ const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
29
+ const ctx: Tool.Context = {
30
+ ...base,
31
+ ask: (req) =>
32
+ Effect.sync(() => {
33
+ items.push(req)
34
+ }),
35
+ }
36
+ return { items, ctx }
37
+ }
38
+
39
+ describe("saeeol external directory boundaries", () => {
40
+ test("asks before accessing outside a repo-root session", async () => {
41
+ await using repo = await tmpdir({ git: true })
42
+ await using outer = await tmpdir()
43
+ const file = path.join(outer.path, "outside.txt")
44
+ const { items, ctx } = asks()
45
+
46
+ await Instance.provide({
47
+ directory: repo.path,
48
+ fn: async () => {
49
+ try {
50
+ await assertExternalDirectory(ctx, file)
51
+ } finally {
52
+ await InstanceStore.disposeInstance(Instance.current)
53
+ }
54
+ },
55
+ })
56
+
57
+ const ext = items.find((item) => item.permission === "external_directory")
58
+ expect(ext).toBeDefined()
59
+ expect(ext!.patterns).toEqual([glob(path.join(outer.path, "*"))])
60
+ expect(ext!.always).toEqual([glob(path.join(outer.path, "*"))])
61
+ expect(ext!.metadata).toMatchObject({ filepath: file, parentDir: outer.path })
62
+ })
63
+
64
+ test("asks when the instance directory is a filesystem root", async () => {
65
+ await using outer = await tmpdir()
66
+ const root = path.parse(outer.path).root
67
+ const file = path.join(outer.path, "outside-root.txt")
68
+ const { items, ctx } = asks()
69
+
70
+ await Instance.provide({
71
+ directory: root,
72
+ fn: async () => {
73
+ try {
74
+ await assertExternalDirectory(ctx, file)
75
+ } finally {
76
+ await InstanceStore.disposeInstance(Instance.current)
77
+ }
78
+ },
79
+ })
80
+
81
+ const ext = items.find((item) => item.permission === "external_directory")
82
+ expect(ext).toBeDefined()
83
+ expect(ext!.patterns).toEqual([glob(path.join(outer.path, "*"))])
84
+ expect(ext!.metadata).toMatchObject({ filepath: file, parentDir: outer.path })
85
+ })
86
+
87
+ test("contains helpers keep dot-prefixed child names internal", () => {
88
+ expect(Filesystem.contains("/project", "/project/..cache/file")).toBe(true)
89
+ expect(AppFileSystem.contains("/a/b", "/a/b/..cache/file")).toBe(true)
90
+ })
91
+
92
+ test("AppFileSystem.contains rejects cross-drive paths on Windows", () => {
93
+ if (process.platform !== "win32") return
94
+ expect(AppFileSystem.contains("C:\\repo", "D:\\outside\\file.txt")).toBe(false)
95
+ })
96
+ })
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, afterEach } from "bun:test"
2
+ import { buildSaeeolHeaders, getFeatureHeader, getEditorNameHeader } from "@saeeol/gateway"
3
+ import { HEADER_FEATURE, ENV_FEATURE, ENV_VERSION, DEFAULT_EDITOR_NAME } from "@saeeol/gateway"
4
+
5
+ describe("getFeatureHeader", () => {
6
+ const original = process.env[ENV_FEATURE]
7
+
8
+ afterEach(() => {
9
+ if (original === undefined) {
10
+ delete process.env[ENV_FEATURE]
11
+ } else {
12
+ process.env[ENV_FEATURE] = original
13
+ }
14
+ })
15
+
16
+ it("returns undefined when env var is not set", () => {
17
+ delete process.env[ENV_FEATURE]
18
+ expect(getFeatureHeader()).toBeUndefined()
19
+ })
20
+
21
+ it("returns the env var value when set", () => {
22
+ process.env[ENV_FEATURE] = "cloud-agent"
23
+ expect(getFeatureHeader()).toBe("cloud-agent")
24
+ })
25
+
26
+ it("returns undefined for empty string", () => {
27
+ process.env[ENV_FEATURE] = ""
28
+ expect(getFeatureHeader()).toBeUndefined()
29
+ })
30
+ })
31
+
32
+ describe("getEditorNameHeader", () => {
33
+ const originalVersion = process.env[ENV_VERSION]
34
+
35
+ afterEach(() => {
36
+ if (originalVersion === undefined) {
37
+ delete process.env[ENV_VERSION]
38
+ } else {
39
+ process.env[ENV_VERSION] = originalVersion
40
+ }
41
+ })
42
+
43
+ it("returns default editor name without version when SAEEOL_VERSION is not set", () => {
44
+ delete process.env[ENV_VERSION]
45
+ expect(getEditorNameHeader()).toBe(DEFAULT_EDITOR_NAME)
46
+ })
47
+
48
+ it("appends version when SAEEOL_VERSION is set", () => {
49
+ process.env[ENV_VERSION] = "1.2.3"
50
+ expect(getEditorNameHeader()).toBe(`${DEFAULT_EDITOR_NAME} 1.2.3`)
51
+ })
52
+ })
53
+
54
+ describe("buildSaeeolHeaders", () => {
55
+ const original = process.env[ENV_FEATURE]
56
+
57
+ afterEach(() => {
58
+ if (original === undefined) {
59
+ delete process.env[ENV_FEATURE]
60
+ } else {
61
+ process.env[ENV_FEATURE] = original
62
+ }
63
+ })
64
+
65
+ it("includes feature header when env var is set", () => {
66
+ process.env[ENV_FEATURE] = "vscode-extension"
67
+ const headers = buildSaeeolHeaders()
68
+ expect(headers[HEADER_FEATURE]).toBe("vscode-extension")
69
+ })
70
+
71
+ it("omits feature header when env var is not set", () => {
72
+ delete process.env[ENV_FEATURE]
73
+ const headers = buildSaeeolHeaders()
74
+ expect(headers[HEADER_FEATURE]).toBeUndefined()
75
+ })
76
+
77
+ it("always includes editor name header", () => {
78
+ delete process.env[ENV_FEATURE]
79
+ const headers = buildSaeeolHeaders()
80
+ expect(headers["X-SAEEOL-EDITORNAME"]).toBe(getEditorNameHeader())
81
+ })
82
+
83
+ it("passes through any feature value from env", () => {
84
+ process.env[ENV_FEATURE] = "custom-feature"
85
+ const headers = buildSaeeolHeaders()
86
+ expect(headers[HEADER_FEATURE]).toBe("custom-feature")
87
+ })
88
+ })
@@ -0,0 +1,191 @@
1
+ import { describe, test, expect } from "bun:test"
2
+ import path from "path"
3
+ import { generateHelp, generateCommandTable } from "../../src/overlay/help"
4
+ import { AcpCommand } from "../../src/cli/cmd/acp"
5
+ import { McpCommand } from "../../src/cli/cmd/mcp"
6
+ import { RunCommand } from "../../src/cli/cmd/run"
7
+ import { GenerateCommand } from "../../src/cli/cmd/generate"
8
+ import { DebugCommand } from "../../src/cli/cmd/debug"
9
+ import { ProvidersCommand } from "../../src/cli/cmd/providers"
10
+ import { AgentCommand } from "../../src/cli/cmd/agent"
11
+ import { UpgradeCommand } from "../../src/cli/cmd/upgrade"
12
+ import { UninstallCommand } from "../../src/cli/cmd/uninstall"
13
+ import { ServeCommand } from "../../src/cli/cmd/serve"
14
+ import { WebCommand } from "../../src/cli/cmd/web"
15
+ import { ModelsCommand } from "../../src/cli/cmd/models"
16
+ import { StatsCommand } from "../../src/cli/cmd/stats"
17
+ import { ExportCommand } from "../../src/cli/cmd/export"
18
+ import { ImportCommand } from "../../src/cli/cmd/import"
19
+ import { PrCommand } from "../../src/cli/cmd/pr"
20
+ import { SessionCommand } from "../../src/cli/cmd/session"
21
+ import { RemoteCommand } from "../../src/cli/cmd/remote"
22
+ import { ConfigCommand as ConfigCLICommand } from "../../src/cli/cmd/config"
23
+ import { PluginCommand } from "../../src/cli/cmd/plug"
24
+ import { DbCommand } from "../../src/cli/cmd/db"
25
+ import { HelpCommand } from "../../src/overlay/help-command"
26
+
27
+ // Stand-in for TuiThreadCommand — the real one imports @opentui/solid which
28
+ // doesn't resolve in the test environment. Only command/describe matter here.
29
+ const TuiStub = {
30
+ command: "$0 [project]",
31
+ describe: "start saeeol tui",
32
+ handler() {},
33
+ }
34
+
35
+ // Stand-in for AttachCommand — same reason as TuiStub above.
36
+ const AttachStub = {
37
+ command: "attach <url>",
38
+ describe: "attach to a running saeeol server",
39
+ handler() {},
40
+ }
41
+
42
+ // Synthetic entry for the yargs built-in .completion() command
43
+ const CompletionStub = {
44
+ command: "completion",
45
+ describe: "generate shell completion script",
46
+ handler() {},
47
+ }
48
+
49
+ const commands = [
50
+ AcpCommand,
51
+ McpCommand,
52
+ TuiStub,
53
+ AttachStub,
54
+ RunCommand,
55
+ GenerateCommand,
56
+ DebugCommand,
57
+ ProvidersCommand,
58
+ AgentCommand,
59
+ UpgradeCommand,
60
+ UninstallCommand,
61
+ ServeCommand,
62
+ WebCommand,
63
+ ModelsCommand,
64
+ StatsCommand,
65
+ ExportCommand,
66
+ ImportCommand,
67
+ PrCommand,
68
+ SessionCommand,
69
+ RemoteCommand,
70
+ DbCommand,
71
+ ConfigCLICommand,
72
+ PluginCommand,
73
+ HelpCommand,
74
+ CompletionStub,
75
+ ] as any[]
76
+
77
+ describe("saeeol help --all (markdown)", () => {
78
+ test("contains ## heading for each known top-level command", async () => {
79
+ const output = await generateHelp({ all: true, format: "md", commands })
80
+ for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent"]) {
81
+ expect(output).toContain(`## saeeol ${cmd}`)
82
+ }
83
+ })
84
+
85
+ test("contains headings for nested subcommands", async () => {
86
+ const output = await generateHelp({ all: true, format: "md", commands })
87
+ expect(output).toContain("saeeol auth login")
88
+ expect(output).toContain("saeeol auth logout")
89
+ expect(output).toContain("saeeol debug config")
90
+ })
91
+ })
92
+
93
+ describe("saeeol help --all (text)", () => {
94
+ test("does NOT contain Markdown ## headings or triple-backtick fences", async () => {
95
+ const output = await generateHelp({ all: true, format: "text", commands })
96
+ expect(output).not.toMatch(/^##\s/m)
97
+ expect(output).not.toContain("```")
98
+ })
99
+
100
+ test("still contains each command name", async () => {
101
+ const output = await generateHelp({ all: true, format: "text", commands })
102
+ for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent"]) {
103
+ expect(output).toContain(`saeeol ${cmd}`)
104
+ }
105
+ })
106
+ })
107
+
108
+ describe("saeeol help <command>", () => {
109
+ test("saeeol help auth contains auth subcommand headings", async () => {
110
+ const output = await generateHelp({ command: "auth", format: "md", commands })
111
+ expect(output).toContain("saeeol auth login")
112
+ expect(output).toContain("saeeol auth logout")
113
+ expect(output).toContain("saeeol auth list")
114
+ })
115
+
116
+ test("saeeol help auth does NOT contain run or debug headings", async () => {
117
+ const output = await generateHelp({ command: "auth", format: "md", commands })
118
+ expect(output).not.toContain("## saeeol run")
119
+ expect(output).not.toContain("## saeeol debug")
120
+ })
121
+ })
122
+
123
+ describe("edge cases", () => {
124
+ test("output contains no ANSI escape sequences", async () => {
125
+ const output = await generateHelp({ all: true, format: "md", commands })
126
+ expect(/\x1b\[/.test(output)).toBe(false)
127
+ })
128
+
129
+ test("saeeol help nonexistent throws unknown command error", async () => {
130
+ await expect(generateHelp({ command: "nonexistent", commands })).rejects.toThrow("unknown command")
131
+ })
132
+ })
133
+
134
+ describe("generateCommandTable", () => {
135
+ test("returns a string containing a markdown table header", async () => {
136
+ const output = await generateCommandTable({ commands })
137
+ expect(output).toContain("| Command | Description |")
138
+ })
139
+
140
+ test("contains rows for known commands", async () => {
141
+ const output = await generateCommandTable({ commands })
142
+ for (const name of ["run", "auth", "debug", "mcp"]) {
143
+ expect(output).toContain(`saeeol ${name}`)
144
+ }
145
+ })
146
+
147
+ test("default command appears as saeeol [project], not $0", async () => {
148
+ const output = await generateCommandTable({ commands })
149
+ expect(output).toContain("`saeeol [project]`")
150
+ expect(output).not.toContain("$0")
151
+ })
152
+
153
+ test("contains no ANSI escape sequences", async () => {
154
+ const output = await generateCommandTable({ commands })
155
+ expect(/\x1b\[/.test(output)).toBe(false)
156
+ })
157
+
158
+ test("skips commands with no describe", async () => {
159
+ const output = await generateCommandTable({ commands })
160
+ expect(output).not.toContain("`saeeol generate`")
161
+ })
162
+
163
+ test("contains saeeol completion row", async () => {
164
+ const output = await generateCommandTable({ commands })
165
+ expect(output).toContain("`saeeol completion`")
166
+ })
167
+
168
+ test("contains saeeol help row", async () => {
169
+ const output = await generateCommandTable({ commands })
170
+ expect(output).toContain("`saeeol help")
171
+ })
172
+ })
173
+
174
+ describe("commands.ts stays in sync with index.ts", () => {
175
+ test("every .command() in index.ts has an entry in the commands array", async () => {
176
+ const index = await Bun.file(path.resolve(import.meta.dir, "../../src/index.ts")).text()
177
+ const barrel = await Bun.file(path.resolve(import.meta.dir, "../../src/overlay/commands.ts")).text()
178
+
179
+ // Match uncommented .command(XxxCommand) calls in index.ts
180
+ const registered = [...index.matchAll(/^\s*\.command\((\w+)\)/gm)].map((m) => m[1]!)
181
+ expect(registered.length).toBeGreaterThan(0)
182
+
183
+ // Extract identifiers inside the exported commands = [...] array, not just anywhere in the file
184
+ const arrayMatch = barrel.match(/export const commands\s*=\s*\[([\s\S]*?)\]/)
185
+ expect(arrayMatch).toBeTruthy()
186
+ const entries = [...arrayMatch![1]!.matchAll(/\b(\w+Command)\b/g)].map((m) => m[1]!)
187
+
188
+ const missing = registered.filter((name) => !entries.includes(name))
189
+ expect(missing).toEqual([])
190
+ })
191
+ })
@@ -0,0 +1,308 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { IgnoreMigrator } from "../../src/overlay/ignore-migrator"
3
+ import { tmpdir } from "../fixture/fixture"
4
+ import path from "path"
5
+
6
+ describe("IgnoreMigrator", () => {
7
+ describe("parseIgnoreContent", () => {
8
+ test("parses basic patterns", () => {
9
+ const content = `
10
+ # Comment
11
+ secrets/
12
+ *.env
13
+ !.env.example
14
+ `
15
+ const patterns = IgnoreMigrator.parseIgnoreContent(content)
16
+
17
+ expect(patterns).toEqual([
18
+ { pattern: "secrets/", negated: false },
19
+ { pattern: "*.env", negated: false },
20
+ { pattern: ".env.example", negated: true },
21
+ ])
22
+ })
23
+
24
+ test("ignores empty lines and comments", () => {
25
+ const content = `
26
+ # This is a comment
27
+ pattern1
28
+
29
+ # Another comment
30
+ pattern2
31
+ `
32
+ const patterns = IgnoreMigrator.parseIgnoreContent(content)
33
+
34
+ expect(patterns).toHaveLength(2)
35
+ expect(patterns[0].pattern).toBe("pattern1")
36
+ expect(patterns[1].pattern).toBe("pattern2")
37
+ })
38
+
39
+ test("handles negation patterns", () => {
40
+ const content = `*.log
41
+ !important.log`
42
+ const patterns = IgnoreMigrator.parseIgnoreContent(content)
43
+
44
+ expect(patterns[0]).toEqual({ pattern: "*.log", negated: false })
45
+ expect(patterns[1]).toEqual({ pattern: "important.log", negated: true })
46
+ })
47
+
48
+ test("handles empty content", () => {
49
+ const patterns = IgnoreMigrator.parseIgnoreContent("")
50
+ expect(patterns).toHaveLength(0)
51
+ })
52
+
53
+ test("handles content with only comments", () => {
54
+ const content = `# Comment 1
55
+ # Comment 2`
56
+ const patterns = IgnoreMigrator.parseIgnoreContent(content)
57
+ expect(patterns).toHaveLength(0)
58
+ })
59
+
60
+ test("trims whitespace from patterns", () => {
61
+ const content = ` secrets/
62
+ *.env `
63
+ const patterns = IgnoreMigrator.parseIgnoreContent(content)
64
+
65
+ expect(patterns[0].pattern).toBe("secrets/")
66
+ expect(patterns[1].pattern).toBe("*.env")
67
+ })
68
+ })
69
+
70
+ describe("convertToGlob", () => {
71
+ test("converts directory pattern", () => {
72
+ expect(IgnoreMigrator.convertToGlob("secrets/")).toBe("secrets/*")
73
+ })
74
+
75
+ test("preserves simple filename pattern", () => {
76
+ // Saeeol's * already matches any path depth
77
+ expect(IgnoreMigrator.convertToGlob("*.env")).toBe("*.env")
78
+ })
79
+
80
+ test("converts rooted patterns", () => {
81
+ expect(IgnoreMigrator.convertToGlob("/root-only")).toBe("root-only")
82
+ })
83
+
84
+ test("preserves patterns with path separators", () => {
85
+ expect(IgnoreMigrator.convertToGlob("src/secrets/")).toBe("src/secrets/*")
86
+ })
87
+
88
+ test("handles double-star patterns", () => {
89
+ expect(IgnoreMigrator.convertToGlob("**/deep/")).toBe("*deep/*")
90
+ })
91
+
92
+ test("preserves simple filename without extension", () => {
93
+ // Saeeol's * already matches any path depth
94
+ expect(IgnoreMigrator.convertToGlob("Dockerfile")).toBe("Dockerfile")
95
+ })
96
+
97
+ test("preserves extension-only pattern", () => {
98
+ expect(IgnoreMigrator.convertToGlob("*.log")).toBe("*.log")
99
+ })
100
+
101
+ test("preserves nested path pattern", () => {
102
+ expect(IgnoreMigrator.convertToGlob("config/secrets.json")).toBe("config/secrets.json")
103
+ })
104
+ })
105
+
106
+ describe("buildPermissionRules", () => {
107
+ test("creates default allow rule", () => {
108
+ const rules = IgnoreMigrator.buildPermissionRules([])
109
+ expect(rules["*"]).toBe("allow")
110
+ })
111
+
112
+ test("creates deny rules for patterns", () => {
113
+ const patterns = [{ pattern: "secrets/", negated: false, source: "project" as const }]
114
+ const rules = IgnoreMigrator.buildPermissionRules(patterns)
115
+
116
+ expect(rules["*"]).toBe("allow")
117
+ expect(rules["secrets/*"]).toBe("deny")
118
+ })
119
+
120
+ test("creates allow rules for negated patterns", () => {
121
+ const patterns = [
122
+ { pattern: "*.env", negated: false, source: "project" as const },
123
+ { pattern: ".env.example", negated: true, source: "project" as const },
124
+ ]
125
+ const rules = IgnoreMigrator.buildPermissionRules(patterns)
126
+
127
+ expect(rules["*.env"]).toBe("deny")
128
+ expect(rules[".env.example"]).toBe("allow")
129
+ })
130
+
131
+ test("handles multiple deny patterns", () => {
132
+ const patterns = [
133
+ { pattern: "secrets/", negated: false, source: "project" as const },
134
+ { pattern: "*.env", negated: false, source: "project" as const },
135
+ { pattern: "*.key", negated: false, source: "project" as const },
136
+ ]
137
+ const rules = IgnoreMigrator.buildPermissionRules(patterns)
138
+
139
+ expect(rules["secrets/*"]).toBe("deny")
140
+ expect(rules["*.env"]).toBe("deny")
141
+ expect(rules["*.key"]).toBe("deny")
142
+ })
143
+
144
+ test("negated patterns override deny patterns", () => {
145
+ const patterns = [
146
+ { pattern: "config/", negated: false, source: "project" as const },
147
+ { pattern: "config/public.json", negated: true, source: "project" as const },
148
+ ]
149
+ const rules = IgnoreMigrator.buildPermissionRules(patterns)
150
+
151
+ expect(rules["config/*"]).toBe("deny")
152
+ expect(rules["config/public.json"]).toBe("allow")
153
+ })
154
+ })
155
+
156
+ describe("migrate", () => {
157
+ test("returns empty permission for project without .saeeolignore", async () => {
158
+ await using tmp = await tmpdir()
159
+
160
+ const result = await IgnoreMigrator.migrate({
161
+ projectDir: tmp.path,
162
+ skipGlobalPaths: true,
163
+ })
164
+
165
+ expect(result.patternCount).toBe(0)
166
+ expect(Object.keys(result.permission)).toHaveLength(0)
167
+ })
168
+
169
+ test("loads project .saeeolignore", async () => {
170
+ await using tmp = await tmpdir({
171
+ init: async (dir) => {
172
+ await Bun.write(path.join(dir, ".saeeolignore"), "secrets/\n*.env")
173
+ },
174
+ })
175
+
176
+ const result = await IgnoreMigrator.migrate({
177
+ projectDir: tmp.path,
178
+ skipGlobalPaths: true,
179
+ })
180
+
181
+ expect(result.patternCount).toBe(2)
182
+ expect(result.permission.read).toBeDefined()
183
+ expect(result.permission.edit).toBeDefined()
184
+ })
185
+
186
+ test("applies patterns to both read and edit", async () => {
187
+ await using tmp = await tmpdir({
188
+ init: async (dir) => {
189
+ await Bun.write(path.join(dir, ".saeeolignore"), "secrets/")
190
+ },
191
+ })
192
+
193
+ const result = await IgnoreMigrator.migrate({
194
+ projectDir: tmp.path,
195
+ skipGlobalPaths: true,
196
+ })
197
+
198
+ const readRules = result.permission.read as Record<string, string>
199
+ const editRules = result.permission.edit as Record<string, string>
200
+
201
+ expect(readRules["secrets/*"]).toBe("deny")
202
+ expect(editRules["secrets/*"]).toBe("deny")
203
+ })
204
+
205
+ test("handles negation patterns correctly", async () => {
206
+ await using tmp = await tmpdir({
207
+ init: async (dir) => {
208
+ await Bun.write(path.join(dir, ".saeeolignore"), "*.env\n!.env.example")
209
+ },
210
+ })
211
+
212
+ const result = await IgnoreMigrator.migrate({
213
+ projectDir: tmp.path,
214
+ skipGlobalPaths: true,
215
+ })
216
+
217
+ const readRules = result.permission.read as Record<string, string>
218
+
219
+ expect(readRules["*.env"]).toBe("deny")
220
+ expect(readRules[".env.example"]).toBe("allow")
221
+ })
222
+
223
+ test("handles complex .saeeolignore file", async () => {
224
+ await using tmp = await tmpdir({
225
+ init: async (dir) => {
226
+ await Bun.write(
227
+ path.join(dir, ".saeeolignore"),
228
+ `# Secrets
229
+ secrets/
230
+ *.env
231
+ !.env.example
232
+
233
+ # Keys
234
+ *.key
235
+ *.pem
236
+
237
+ # Config
238
+ /config/private/
239
+ !config/private/public.json
240
+ `,
241
+ )
242
+ },
243
+ })
244
+
245
+ const result = await IgnoreMigrator.migrate({
246
+ projectDir: tmp.path,
247
+ skipGlobalPaths: true,
248
+ })
249
+
250
+ expect(result.patternCount).toBe(7)
251
+
252
+ const readRules = result.permission.read as Record<string, string>
253
+ expect(readRules["*"]).toBe("allow")
254
+ expect(readRules["secrets/*"]).toBe("deny")
255
+ expect(readRules["*.env"]).toBe("deny")
256
+ expect(readRules[".env.example"]).toBe("allow")
257
+ expect(readRules["*.key"]).toBe("deny")
258
+ expect(readRules["*.pem"]).toBe("deny")
259
+ expect(readRules["config/private/*"]).toBe("deny")
260
+ expect(readRules["config/private/public.json"]).toBe("allow")
261
+ })
262
+
263
+ test("returns empty warnings array", async () => {
264
+ await using tmp = await tmpdir({
265
+ init: async (dir) => {
266
+ await Bun.write(path.join(dir, ".saeeolignore"), "secrets/")
267
+ },
268
+ })
269
+
270
+ const result = await IgnoreMigrator.migrate({
271
+ projectDir: tmp.path,
272
+ skipGlobalPaths: true,
273
+ })
274
+
275
+ expect(result.warnings).toHaveLength(0)
276
+ })
277
+ })
278
+
279
+ describe("loadIgnoreConfig", () => {
280
+ test("returns empty object for project without .saeeolignore", async () => {
281
+ await using tmp = await tmpdir()
282
+
283
+ const permission = await IgnoreMigrator.loadIgnoreConfig(tmp.path, true)
284
+
285
+ expect(Object.keys(permission)).toHaveLength(0)
286
+ })
287
+
288
+ test("returns permission config for project with .saeeolignore", async () => {
289
+ await using tmp = await tmpdir({
290
+ init: async (dir) => {
291
+ await Bun.write(path.join(dir, ".saeeolignore"), "secrets/")
292
+ },
293
+ })
294
+
295
+ const permission = await IgnoreMigrator.loadIgnoreConfig(tmp.path, true)
296
+
297
+ expect(permission.read).toBeDefined()
298
+ expect(permission.edit).toBeDefined()
299
+ })
300
+
301
+ test("handles errors gracefully", async () => {
302
+ // Pass a non-existent directory
303
+ const permission = await IgnoreMigrator.loadIgnoreConfig("/non/existent/path", true)
304
+
305
+ expect(Object.keys(permission)).toHaveLength(0)
306
+ })
307
+ })
308
+ })