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,89 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { ProviderTransform } from "../../src/provider/transform"
3
+
4
+ function mockModel(overrides: Partial<any> = {}): any {
5
+ return {
6
+ id: "test/test-model",
7
+ providerID: "test",
8
+ api: {
9
+ id: "test-model",
10
+ url: "https://api.test.com",
11
+ npm: "@ai-sdk/anthropic",
12
+ },
13
+ name: "Test Model",
14
+ capabilities: {
15
+ temperature: true,
16
+ reasoning: true,
17
+ attachment: true,
18
+ toolcall: true,
19
+ input: { text: true, audio: false, image: true, video: false, pdf: false },
20
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
21
+ interleaved: false,
22
+ },
23
+ cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
24
+ limit: { context: 200_000, output: 64_000 },
25
+ status: "active",
26
+ options: {},
27
+ headers: {},
28
+ release_date: "2024-01-01",
29
+ ...overrides,
30
+ }
31
+ }
32
+
33
+ describe("ProviderTransform.variants - Claude Opus 4.7", () => {
34
+ test("opus-4-7 returns adaptive thinking variants including xhigh (native anthropic)", () => {
35
+ const model = mockModel({
36
+ api: {
37
+ id: "claude-opus-4-7",
38
+ url: "https://api.anthropic.com",
39
+ npm: "@ai-sdk/anthropic",
40
+ },
41
+ })
42
+ const result = ProviderTransform.variants(model)
43
+ expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
44
+ expect(result.xhigh).toEqual({
45
+ thinking: { type: "adaptive", display: "summarized" },
46
+ effort: "xhigh",
47
+ })
48
+ })
49
+
50
+ test("opus-4.7 dot-form returns adaptive thinking variants via @ai-sdk/gateway", () => {
51
+ const model = mockModel({
52
+ id: "anthropic/claude-opus-4-7",
53
+ api: {
54
+ id: "anthropic/claude-opus-4.7",
55
+ url: "https://gateway.ai",
56
+ npm: "@ai-sdk/gateway",
57
+ },
58
+ })
59
+ const result = ProviderTransform.variants(model)
60
+ expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
61
+ })
62
+
63
+ test("opus-4-7 on bedrock returns adaptive reasoningConfig with xhigh", () => {
64
+ const model = mockModel({
65
+ api: {
66
+ id: "anthropic.claude-opus-4-7",
67
+ url: "https://bedrock.amazonaws.com",
68
+ npm: "@ai-sdk/amazon-bedrock",
69
+ },
70
+ })
71
+ const result = ProviderTransform.variants(model)
72
+ expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
73
+ expect(result.xhigh).toEqual({
74
+ reasoningConfig: { type: "adaptive", maxReasoningEffort: "xhigh", display: "summarized" },
75
+ })
76
+ })
77
+
78
+ test("opus-4-6 keeps original adaptive efforts without xhigh", () => {
79
+ const model = mockModel({
80
+ api: {
81
+ id: "claude-opus-4-6",
82
+ url: "https://api.anthropic.com",
83
+ npm: "@ai-sdk/anthropic",
84
+ },
85
+ })
86
+ const result = ProviderTransform.variants(model)
87
+ expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
88
+ })
89
+ })
@@ -0,0 +1,91 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { splitDiffHunks } from "../../src/overlay/tui/diff"
3
+
4
+ describe("splitDiffHunks", () => {
5
+ test("returns original diff when there are no hunks", () => {
6
+ const diff = "--- a/file.ts\n+++ b/file.ts"
7
+ expect(splitDiffHunks(diff)).toEqual([diff])
8
+ })
9
+
10
+ test("returns original diff when there is one hunk", () => {
11
+ const diff = ["--- a/file.ts", "+++ b/file.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE", " two"].join("\n")
12
+
13
+ expect(splitDiffHunks(diff)).toEqual([diff])
14
+ })
15
+
16
+ test("ignores header-like content lines inside a hunk", () => {
17
+ const diff = [
18
+ "--- a/file.ts",
19
+ "+++ b/file.ts",
20
+ "@@ -1,1 +1,1 @@",
21
+ "--- not-a-header",
22
+ "+++ still-not-a-header",
23
+ ].join("\n")
24
+
25
+ expect(splitDiffHunks(diff)).toEqual([diff])
26
+ })
27
+
28
+ test("splits multi-hunk diff and preserves headers", () => {
29
+ const diff = [
30
+ "--- a/file.ts",
31
+ "+++ b/file.ts",
32
+ "@@ -1,2 +1,2 @@",
33
+ "-one",
34
+ "+ONE",
35
+ " two",
36
+ "@@ -10,2 +10,2 @@",
37
+ "-ten",
38
+ "+TEN",
39
+ " eleven",
40
+ ].join("\n")
41
+
42
+ expect(splitDiffHunks(diff)).toEqual([
43
+ ["--- a/file.ts", "+++ b/file.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE", " two"].join("\n"),
44
+ ["--- a/file.ts", "+++ b/file.ts", "@@ -10,2 +10,2 @@", "-ten", "+TEN", " eleven"].join("\n"),
45
+ ])
46
+ })
47
+
48
+ test("splits concatenated multi-file diff with per-file headers", () => {
49
+ const diff = [
50
+ "--- a/one.ts",
51
+ "+++ b/one.ts",
52
+ "@@ -1,2 +1,2 @@",
53
+ "-one",
54
+ "+ONE",
55
+ "--- a/two.ts",
56
+ "+++ b/two.ts",
57
+ "@@ -1,2 +1,2 @@",
58
+ "-two",
59
+ "+TWO",
60
+ ].join("\n")
61
+
62
+ expect(splitDiffHunks(diff)).toEqual([
63
+ ["--- a/one.ts", "+++ b/one.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE"].join("\n"),
64
+ ["--- a/two.ts", "+++ b/two.ts", "@@ -1,2 +1,2 @@", "-two", "+TWO"].join("\n"),
65
+ ])
66
+ })
67
+
68
+ test("splits multi-file diff when first file has multiple hunks", () => {
69
+ const diff = [
70
+ "--- a/one.ts",
71
+ "+++ b/one.ts",
72
+ "@@ -1,2 +1,2 @@",
73
+ "-one",
74
+ "+ONE",
75
+ "@@ -10,2 +10,2 @@",
76
+ "-ten",
77
+ "+TEN",
78
+ "--- a/two.ts",
79
+ "+++ b/two.ts",
80
+ "@@ -3,2 +3,2 @@",
81
+ "-two",
82
+ "+TWO",
83
+ ].join("\n")
84
+
85
+ expect(splitDiffHunks(diff)).toEqual([
86
+ ["--- a/one.ts", "+++ b/one.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE"].join("\n"),
87
+ ["--- a/one.ts", "+++ b/one.ts", "@@ -10,2 +10,2 @@", "-ten", "+TEN"].join("\n"),
88
+ ["--- a/two.ts", "+++ b/two.ts", "@@ -3,2 +3,2 @@", "-two", "+TWO"].join("\n"),
89
+ ])
90
+ })
91
+ })
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { SaeeolSessionTuiSync } from "../../src/overlay/session/tui-sync"
3
+
4
+ type Message = {
5
+ role: string
6
+ model?: { providerID: string; modelID: string; variant?: string }
7
+ parts?: readonly { type: string }[]
8
+ }
9
+
10
+ function syncVariant(input: { current: string | undefined; message: Message; parts?: readonly { type: string }[] }) {
11
+ if (!SaeeolSessionTuiSync.model({ role: input.message.role, parts: input.parts })) return input.current
12
+ return input.message.model?.variant ?? "default"
13
+ }
14
+
15
+ describe("SaeeolSessionTuiSync.model", () => {
16
+ test("syncs normal user messages", () => {
17
+ expect(SaeeolSessionTuiSync.model({ role: "user", parts: [{ type: "text" }] })).toBe(true)
18
+ })
19
+
20
+ test("skips compaction marker user messages", () => {
21
+ expect(SaeeolSessionTuiSync.model({ role: "user", parts: [{ type: "compaction" }] })).toBe(false)
22
+ })
23
+
24
+ test("skips messages before parts load", () => {
25
+ expect(SaeeolSessionTuiSync.model({ role: "user" })).toBe(false)
26
+ })
27
+
28
+ test("skips messages checked with stored parts", () => {
29
+ const msg = { role: "user" }
30
+ const parts = [{ type: "compaction" }]
31
+
32
+ expect(SaeeolSessionTuiSync.model({ role: msg.role, parts })).toBe(false)
33
+ })
34
+
35
+ test("skips non-user messages", () => {
36
+ expect(SaeeolSessionTuiSync.model({ role: "assistant", parts: [{ type: "text" }] })).toBe(false)
37
+ })
38
+
39
+ test("preserves thinking level after /compact", () => {
40
+ const msg = {
41
+ role: "user",
42
+ model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" },
43
+ parts: [{ type: "compaction" }],
44
+ }
45
+
46
+ expect(syncVariant({ current: "high", message: msg, parts: msg.parts })).toBe("high")
47
+ })
48
+
49
+ test("preserves thinking level when compaction parts are stored separately", () => {
50
+ const msg = {
51
+ role: "user",
52
+ model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" },
53
+ }
54
+ const parts = [{ type: "compaction" }]
55
+
56
+ expect(parts.some((part) => part.type === "compaction")).toBe(true)
57
+ expect(syncVariant({ current: "high", message: msg, parts })).toBe("high")
58
+ })
59
+
60
+ test("waits for normal user message parts before syncing", () => {
61
+ const msg = {
62
+ role: "user",
63
+ model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", variant: "max" },
64
+ }
65
+ const parts = [{ type: "text" }]
66
+
67
+ expect(syncVariant({ current: "high", message: msg })).toBe("high")
68
+ expect(syncVariant({ current: "high", message: msg, parts })).toBe("max")
69
+ })
70
+
71
+ test("still updates thinking level from normal user messages", () => {
72
+ const msg = {
73
+ role: "user",
74
+ model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", variant: "max" },
75
+ parts: [{ type: "text" }],
76
+ }
77
+
78
+ expect(syncVariant({ current: "high", message: msg, parts: msg.parts })).toBe("max")
79
+ })
80
+ })
@@ -0,0 +1,141 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { normalizeUrls } from "../../../src/overlay/util/url"
3
+
4
+ describe("normalizeUrls", () => {
5
+ describe("homograph / IDN conversion", () => {
6
+ test("converts Cyrillic look-alike in hostname to punycode", () => {
7
+ // Cyrillic 'а' (U+0430) is visually identical to Latin 'a'
8
+ const input = "https://\u0430pitest.com/status"
9
+ const result = normalizeUrls(input)
10
+ expect(result).toBe("https://xn--pitest-2nf.com/status")
11
+ expect(result).not.toContain("\u0430")
12
+ })
13
+
14
+ test("converts mixed-script hostname to punycode", () => {
15
+ // Mix of Latin and Cyrillic in the same label
16
+ const input = "https://\u0430pitest.com/path"
17
+ expect(normalizeUrls(input)).not.toContain("\u0430")
18
+ })
19
+
20
+ test("converts fully unicode TLD to punycode", () => {
21
+ const input = "https://example.\u4e2d\u56fd"
22
+ const result = normalizeUrls(input)
23
+ expect(result).toMatch(/^https:\/\/example\.xn--/)
24
+ })
25
+
26
+ test("handles http scheme as well as https", () => {
27
+ const input = "http://\u0430pitest.com/path"
28
+ const result = normalizeUrls(input)
29
+ expect(result).not.toContain("\u0430")
30
+ expect(result).toMatch(/^http:\/\/xn--/)
31
+ })
32
+ })
33
+
34
+ describe("plain ASCII URLs are unchanged", () => {
35
+ test("leaves a URL with a path untouched", () => {
36
+ const url = "https://apitest.com/status"
37
+ expect(normalizeUrls(url)).toBe(url)
38
+ })
39
+
40
+ test("leaves a URL with path and query string untouched", () => {
41
+ const url = "http://example.com/foo?bar=1&baz=2"
42
+ expect(normalizeUrls(url)).toBe(url)
43
+ })
44
+
45
+ test("leaves a localhost URL with port untouched", () => {
46
+ const url = "http://localhost:3000/api"
47
+ expect(normalizeUrls(url)).toBe(url)
48
+ })
49
+
50
+ test("leaves a bare origin untouched (no trailing slash added)", () => {
51
+ // Regression: new URL("https://example.com").href === "https://example.com/"
52
+ // The old implementation using href would mutate bare origins by adding "/".
53
+ const url = "https://example.com"
54
+ expect(normalizeUrls(url)).toBe(url)
55
+ })
56
+ })
57
+
58
+ describe("trailing sentence punctuation is not consumed into the URL", () => {
59
+ test("period at end of sentence is not consumed (was: adds trailing slash)", () => {
60
+ // "see https://example.com." — the period ends the sentence, not the URL.
61
+ // Old behaviour (bug): returned "see https://example.com./"
62
+ expect(normalizeUrls("see https://example.com.")).toBe("see https://example.com.")
63
+ })
64
+
65
+ test("exclamation mark at end of sentence is not consumed", () => {
66
+ expect(normalizeUrls("visit https://example.com!")).toBe("visit https://example.com!")
67
+ })
68
+
69
+ test("comma after URL in a list is not consumed", () => {
70
+ expect(normalizeUrls("check https://example.com, then continue")).toBe("check https://example.com, then continue")
71
+ })
72
+
73
+ test("closing parenthesis after URL is not consumed", () => {
74
+ expect(normalizeUrls("(see https://example.com)")).toBe("(see https://example.com)")
75
+ })
76
+
77
+ test("trailing punctuation after an IDN URL is stripped correctly and punycode applied", () => {
78
+ // Trailing period on a homograph URL: period is sentence punctuation, not part of the URL.
79
+ const input = "see https://\u0430pitest.com."
80
+ const result = normalizeUrls(input)
81
+ expect(result).toBe("see https://xn--pitest-2nf.com.")
82
+ expect(result).not.toContain("\u0430")
83
+ })
84
+ })
85
+
86
+ describe("URL embedded in a bash command string", () => {
87
+ test("normalizes the URL portion of a curl command", () => {
88
+ const input = "curl https://\u0430pitest.com/status"
89
+ const result = normalizeUrls(input)
90
+ expect(result).toMatch(/^curl https:\/\/xn--/)
91
+ expect(result).not.toContain("\u0430")
92
+ })
93
+
94
+ test("preserves flags and pipe around the URL", () => {
95
+ const input = "curl -sSf https://\u0430pitest.com/status | bash"
96
+ const result = normalizeUrls(input)
97
+ expect(result).toMatch(/^curl -sSf /)
98
+ expect(result).toContain("| bash")
99
+ })
100
+
101
+ test("normalizes multiple URLs in a single command", () => {
102
+ const input = "curl https://\u0430pitest.com/a && curl https://\u0430pitest.com/b"
103
+ const result = normalizeUrls(input)
104
+ expect(result.match(/xn--/g)?.length).toBe(2)
105
+ expect(result).not.toContain("\u0430")
106
+ })
107
+
108
+ test("leaves a plain-ASCII command entirely unchanged", () => {
109
+ const input = "curl -sSf https://saeeol.ai/update.sh | bash"
110
+ expect(normalizeUrls(input)).toBe(input)
111
+ })
112
+ })
113
+
114
+ describe("edge cases", () => {
115
+ test("returns empty string unchanged", () => {
116
+ expect(normalizeUrls("")).toBe("")
117
+ })
118
+
119
+ test("returns text with no URLs unchanged", () => {
120
+ const text = "just some plain text without links"
121
+ expect(normalizeUrls(text)).toBe(text)
122
+ })
123
+
124
+ test("does not alter non-http/https schemes", () => {
125
+ const text = "ftp://example.com and file:///tmp/foo"
126
+ expect(normalizeUrls(text)).toBe(text)
127
+ })
128
+
129
+ test("preserves path, query string, and fragment after IDN conversion", () => {
130
+ const input = "https://\u0430pitest.com/path?q=1#anchor"
131
+ const result = normalizeUrls(input)
132
+ expect(result).toMatch(/xn--/)
133
+ expect(result).toContain("/path?q=1#anchor")
134
+ })
135
+
136
+ test("preserves a URL that fails to parse verbatim", () => {
137
+ const malformed = "https://[unclosed"
138
+ expect(normalizeUrls(malformed)).toBe(malformed)
139
+ })
140
+ })
141
+ })
@@ -0,0 +1,261 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { WorkflowsMigrator } from "../../src/overlay/workflows-migrator"
3
+ import { tmpdir } from "../fixture/fixture"
4
+ import path from "path"
5
+
6
+ async function withHome<T>(home: string, fn: () => Promise<T>): Promise<T> {
7
+ const prev = process.env.HOME
8
+ process.env.HOME = home
9
+ try {
10
+ return await fn()
11
+ } finally {
12
+ if (prev) process.env.HOME = prev
13
+ else delete process.env.HOME
14
+ }
15
+ }
16
+
17
+ describe("WorkflowsMigrator", () => {
18
+ describe("extractNameFromFilename", () => {
19
+ test("extracts name from simple filename", () => {
20
+ expect(WorkflowsMigrator.extractNameFromFilename("code-review.md")).toBe("code-review")
21
+ })
22
+
23
+ test("extracts name from path", () => {
24
+ expect(WorkflowsMigrator.extractNameFromFilename("/path/to/my-workflow.md")).toBe("my-workflow")
25
+ })
26
+
27
+ test("handles filename without extension", () => {
28
+ expect(WorkflowsMigrator.extractNameFromFilename("workflow")).toBe("workflow")
29
+ })
30
+ })
31
+
32
+ describe("extractDescription", () => {
33
+ test("extracts description from first paragraph after title", () => {
34
+ const content = `# My Workflow
35
+
36
+ This is the description of the workflow.
37
+
38
+ ## Steps
39
+
40
+ 1. Do something`
41
+
42
+ expect(WorkflowsMigrator.extractDescription(content)).toBe("This is the description of the workflow.")
43
+ })
44
+
45
+ test("returns undefined when no description found", () => {
46
+ const content = `# My Workflow`
47
+ expect(WorkflowsMigrator.extractDescription(content)).toBeUndefined()
48
+ })
49
+
50
+ test("limits description to 200 characters", () => {
51
+ const longDescription = "A".repeat(300)
52
+ const content = `# Title
53
+
54
+ ${longDescription}`
55
+
56
+ const result = WorkflowsMigrator.extractDescription(content)
57
+ expect(result?.length).toBe(200)
58
+ })
59
+
60
+ test("skips empty lines after title", () => {
61
+ const content = `# Title
62
+
63
+
64
+ Actual description here.`
65
+
66
+ expect(WorkflowsMigrator.extractDescription(content)).toBe("Actual description here.")
67
+ })
68
+ })
69
+
70
+ describe("discoverWorkflows", () => {
71
+ test("discovers project workflows", async () => {
72
+ await using tmp = await tmpdir({
73
+ init: async (dir) => {
74
+ const workflowsDir = path.join(dir, ".saeeol", "workflows")
75
+ await Bun.write(path.join(workflowsDir, "test-workflow.md"), "# Test\n\nDescription")
76
+ },
77
+ })
78
+
79
+ const workflows = await WorkflowsMigrator.discoverWorkflows(tmp.path, true)
80
+
81
+ expect(workflows).toHaveLength(1)
82
+ expect(workflows[0].name).toBe("test-workflow")
83
+ expect(workflows[0].source).toBe("project")
84
+ })
85
+
86
+ test("discovers workflows from legacy .saeeol/workflows/", async () => {
87
+ await using tmp = await tmpdir({
88
+ init: async (dir) => {
89
+ const workflowsDir = path.join(dir, ".saeeol", "workflows")
90
+ await Bun.write(path.join(workflowsDir, "legacy-workflow.md"), "# Legacy\n\nLegacy workflow")
91
+ },
92
+ })
93
+
94
+ const workflows = await WorkflowsMigrator.discoverWorkflows(tmp.path, true)
95
+
96
+ expect(workflows).toHaveLength(1)
97
+ expect(workflows[0].name).toBe("legacy-workflow")
98
+ expect(workflows[0].source).toBe("project")
99
+ })
100
+
101
+ test("returns empty array when no workflows directory exists", async () => {
102
+ await using tmp = await tmpdir()
103
+
104
+ const workflows = await WorkflowsMigrator.discoverWorkflows(tmp.path, true)
105
+
106
+ expect(workflows).toHaveLength(0)
107
+ })
108
+
109
+ test("only discovers .md files", async () => {
110
+ await using tmp = await tmpdir({
111
+ init: async (dir) => {
112
+ const workflowsDir = path.join(dir, ".saeeol", "workflows")
113
+ await Bun.write(path.join(workflowsDir, "workflow.md"), "# Workflow")
114
+ await Bun.write(path.join(workflowsDir, "readme.txt"), "Not a workflow")
115
+ await Bun.write(path.join(workflowsDir, "config.json"), "{}")
116
+ },
117
+ })
118
+
119
+ const workflows = await WorkflowsMigrator.discoverWorkflows(tmp.path, true)
120
+
121
+ expect(workflows).toHaveLength(1)
122
+ expect(workflows[0].name).toBe("workflow")
123
+ })
124
+
125
+ test("discovers global workflows from ~/.saeeol/workflows/", async () => {
126
+ await using tmp = await tmpdir({
127
+ init: async (dir) => {
128
+ await Bun.write(path.join(dir, ".saeeol", "workflows", "global.md"), "# Global\n\nGlobal workflow")
129
+ await Bun.write(path.join(dir, "repo", "README.md"), "repo")
130
+ },
131
+ })
132
+
133
+ const workflows = await withHome(tmp.path, () => WorkflowsMigrator.discoverWorkflows(path.join(tmp.path, "repo")))
134
+
135
+ expect(
136
+ workflows.some((w) => w.source === "global" && w.path.includes(path.join(".saeeol", "workflows", "global.md"))),
137
+ ).toBe(true)
138
+ })
139
+
140
+ test("applies markdown substitutions to workflow content", async () => {
141
+ process.env.SAEEOL_WORKFLOW_TEST = "env content"
142
+ await using tmp = await tmpdir({
143
+ init: async (dir) => {
144
+ const workflowsDir = path.join(dir, ".saeeol", "workflows")
145
+ await Bun.write(path.join(dir, "guide.md"), "file content")
146
+ await Bun.write(
147
+ path.join(workflowsDir, "workflow.md"),
148
+ ["# Workflow", "", "{file:../../guide.md}", "{env:SAEEOL_WORKFLOW_TEST}"].join("\n"),
149
+ )
150
+ },
151
+ })
152
+
153
+ try {
154
+ const workflows = await WorkflowsMigrator.discoverWorkflows(tmp.path, true)
155
+
156
+ expect(workflows[0].content).toContain("file content")
157
+ expect(workflows[0].content).toContain("env content")
158
+ } finally {
159
+ delete process.env.SAEEOL_WORKFLOW_TEST
160
+ }
161
+ })
162
+ })
163
+
164
+ describe("convertToCommand", () => {
165
+ test("converts workflow to command format", () => {
166
+ const workflow: WorkflowsMigrator.SaeeolWorkflow = {
167
+ name: "code-review",
168
+ path: "/path/to/code-review.md",
169
+ content: "# Code Review\n\nReview the code changes.\n\n## Steps\n\n1. Check",
170
+ source: "project",
171
+ }
172
+
173
+ const command = WorkflowsMigrator.convertToCommand(workflow)
174
+
175
+ expect(command.template).toBe(workflow.content)
176
+ expect(command.description).toBe("Review the code changes.")
177
+ })
178
+
179
+ test("uses fallback description when none found", () => {
180
+ const workflow: WorkflowsMigrator.SaeeolWorkflow = {
181
+ name: "simple",
182
+ path: "/path/to/simple.md",
183
+ content: "# Simple",
184
+ source: "project",
185
+ }
186
+
187
+ const command = WorkflowsMigrator.convertToCommand(workflow)
188
+
189
+ expect(command.description).toBe("Workflow: simple")
190
+ })
191
+ })
192
+
193
+ describe("migrate", () => {
194
+ test("migrates project workflows to commands", async () => {
195
+ await using tmp = await tmpdir({
196
+ init: async (dir) => {
197
+ const workflowsDir = path.join(dir, ".saeeol", "workflows")
198
+ await Bun.write(
199
+ path.join(workflowsDir, "code-review.md"),
200
+ "# Code Review\n\nPerform a code review.\n\n## Steps\n\n1. Review",
201
+ )
202
+ },
203
+ })
204
+
205
+ const result = await WorkflowsMigrator.migrate({ projectDir: tmp.path, skipGlobalPaths: true })
206
+
207
+ expect(Object.keys(result.commands)).toHaveLength(1)
208
+ expect(result.commands["code-review"]).toBeDefined()
209
+ expect(result.commands["code-review"].template).toContain("# Code Review")
210
+ expect(result.commands["code-review"].description).toBe("Perform a code review.")
211
+ })
212
+
213
+ test("returns empty commands when no workflows exist", async () => {
214
+ await using tmp = await tmpdir()
215
+
216
+ const result = await WorkflowsMigrator.migrate({ projectDir: tmp.path, skipGlobalPaths: true })
217
+
218
+ expect(Object.keys(result.commands)).toHaveLength(0)
219
+ expect(result.warnings).toHaveLength(0)
220
+ })
221
+
222
+ test("migrates multiple workflows", async () => {
223
+ await using tmp = await tmpdir({
224
+ init: async (dir) => {
225
+ const workflowsDir = path.join(dir, ".saeeol", "workflows")
226
+ await Bun.write(path.join(workflowsDir, "review.md"), "# Review\n\nReview code")
227
+ await Bun.write(path.join(workflowsDir, "deploy.md"), "# Deploy\n\nDeploy app")
228
+ },
229
+ })
230
+
231
+ const result = await WorkflowsMigrator.migrate({ projectDir: tmp.path, skipGlobalPaths: true })
232
+
233
+ expect(Object.keys(result.commands)).toHaveLength(2)
234
+ expect(result.commands["review"]).toBeDefined()
235
+ expect(result.commands["deploy"]).toBeDefined()
236
+ })
237
+
238
+ test("project workflows override global workflows with same name", async () => {
239
+ await using tmp = await tmpdir({
240
+ init: async (dir) => {
241
+ // Create a "global" directory to simulate global workflows
242
+ const globalDir = path.join(dir, "global-workflows")
243
+ await Bun.write(path.join(globalDir, "shared.md"), "# Shared\n\nGlobal version")
244
+
245
+ // Create project workflows
246
+ const projectDir = path.join(dir, ".saeeol", "workflows")
247
+ await Bun.write(path.join(projectDir, "shared.md"), "# Shared\n\nProject version")
248
+
249
+ return globalDir
250
+ },
251
+ })
252
+
253
+ // Note: We can't easily test global workflow override without mocking the home directory
254
+ // This test verifies the deduplication logic works for project workflows
255
+ const result = await WorkflowsMigrator.migrate({ projectDir: tmp.path, skipGlobalPaths: true })
256
+
257
+ expect(Object.keys(result.commands)).toHaveLength(1)
258
+ expect(result.commands["shared"].template).toContain("Project version")
259
+ })
260
+ })
261
+ })