saeeol 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/.turbo/turbo-typecheck.log +1 -0
  2. package/AGENTS.md +72 -0
  3. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  4. package/Dockerfile +18 -0
  5. package/assets/saeeol.ico +0 -0
  6. package/bin/saeeol.cjs +0 -0
  7. package/database.db +0 -0
  8. package/drizzle.config.ts +10 -0
  9. package/git +0 -0
  10. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  11. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  12. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  13. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  14. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  15. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  16. package/migration/20260225215848_workspace/migration.sql +7 -0
  17. package/migration/20260225215848_workspace/snapshot.json +959 -0
  18. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  19. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  20. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  21. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  22. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  23. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  24. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  25. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  26. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  27. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  28. package/migration/20260323234822_events/migration.sql +13 -0
  29. package/migration/20260323234822_events/snapshot.json +1271 -0
  30. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  31. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  32. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  33. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  34. package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
  35. package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
  36. package/migration/20260428004200_add_session_path/migration.sql +1 -0
  37. package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
  38. package/npm/bin/saeeol +42 -0
  39. package/npm/package.json +39 -0
  40. package/npm/postinstall.js +162 -0
  41. package/package.json +201 -207
  42. package/parsers-config.ts +289 -0
  43. package/script/build.ts +393 -0
  44. package/script/check-migrations.ts +16 -0
  45. package/script/fix-node-pty.ts +34 -0
  46. package/script/generate.ts +23 -0
  47. package/script/postinstall.mjs +189 -0
  48. package/script/publish.ts +200 -0
  49. package/script/run-workspace-server +106 -0
  50. package/script/schema.ts +63 -0
  51. package/script/test-runner.ts +420 -0
  52. package/script/time.ts +6 -0
  53. package/script/trace-imports.ts +153 -0
  54. package/script/upgrade-opentui.ts +64 -0
  55. package/scripts/diff-sdk-types.sh +52 -0
  56. package/specs/effect/facades.md +221 -0
  57. package/specs/effect/http-api.md +401 -0
  58. package/specs/effect/instance-context.md +309 -0
  59. package/specs/effect/loose-ends.md +34 -0
  60. package/specs/effect/migration.md +299 -0
  61. package/specs/effect/routes.md +64 -0
  62. package/specs/effect/schema.md +399 -0
  63. package/specs/effect/server-package.md +668 -0
  64. package/specs/effect/tools.md +90 -0
  65. package/specs/tui-plugins.md +433 -0
  66. package/specs/v2/api.ts +67 -0
  67. package/specs/v2/keymappings.md +10 -0
  68. package/specs/v2/message-shape.md +136 -0
  69. package/src/acp/agent-message.ts +1 -1
  70. package/src/acp/agent-utils.ts +1 -1
  71. package/src/boxes/ansi.ts +17 -0
  72. package/src/boxes/atomic-write.ts +35 -0
  73. package/src/boxes/b64.ts +58 -0
  74. package/src/boxes/bash-security.ts +129 -0
  75. package/src/boxes/bom.ts +18 -0
  76. package/src/boxes/cancel.ts +16 -0
  77. package/src/boxes/chop.ts +12 -0
  78. package/src/boxes/clamp.ts +3 -0
  79. package/src/boxes/compact.ts +9 -0
  80. package/src/boxes/cost-tracker.ts +116 -0
  81. package/src/boxes/dataurl.ts +29 -0
  82. package/src/boxes/delay.ts +27 -0
  83. package/src/boxes/diff-apply.ts +53 -0
  84. package/src/boxes/disposable.ts +13 -0
  85. package/src/boxes/err.ts +34 -0
  86. package/src/boxes/human.ts +47 -0
  87. package/src/boxes/iife.ts +9 -0
  88. package/src/boxes/latch.ts +8 -0
  89. package/src/boxes/memory.ts +198 -0
  90. package/src/boxes/net.ts +16 -0
  91. package/src/boxes/plural.ts +4 -0
  92. package/src/boxes/puny.ts +21 -0
  93. package/src/boxes/retry.ts +49 -0
  94. package/src/boxes/rwlock.ts +41 -0
  95. package/src/boxes/schedule.ts +71 -0
  96. package/src/boxes/scope.ts +21 -0
  97. package/src/boxes/tokens.ts +9 -0
  98. package/src/boxes/ttl-cache.ts +63 -0
  99. package/src/boxes/typed-event.ts +51 -0
  100. package/src/boxes/uid.ts +50 -0
  101. package/src/boxes/wave6.test.ts +296 -0
  102. package/src/boxes/wildcard.ts +58 -0
  103. package/src/bus/global.ts +1 -1
  104. package/src/cli/cmd/github-run-api.ts +2 -2
  105. package/src/cli/cmd/run-events.ts +2 -2
  106. package/src/cli/cmd/tui/component/logo.tsx +1 -1
  107. package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
  108. package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
  109. package/src/cli/cmd/tui/context/app/editor.ts +1 -1
  110. package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
  111. package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
  112. package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
  113. package/src/overlay/cost-tracker/format.ts +1 -1
  114. package/src/overlay/cost-tracker/index.ts +4 -4
  115. package/src/overlay/cost-tracker/state.ts +2 -2
  116. package/src/overlay/cost-tracker/types.ts +2 -2
  117. package/src/overlay/memory/age.ts +1 -1
  118. package/src/overlay/memory/index.ts +4 -4
  119. package/src/overlay/memory/paths.ts +2 -2
  120. package/src/overlay/memory/scan.ts +1 -1
  121. package/src/overlay/memory/types.ts +2 -2
  122. package/src/overlay/tool/bash-security.ts +3 -3
  123. package/src/overlay/util/url.ts +1 -1
  124. package/src/plugin/codex-auth.ts +1 -1
  125. package/src/provider/model-cache.ts +2 -2
  126. package/src/provider/provider-resolve.ts +3 -3
  127. package/src/provider/transform-message.ts +1 -1
  128. package/src/server/routes/game.ts +284 -0
  129. package/src/server/server.ts +2 -0
  130. package/src/session/core/compaction/compaction-helpers.ts +1 -1
  131. package/src/session/core/compaction/compaction.ts +1 -1
  132. package/src/session/core/session.ts +2 -0
  133. package/src/sessions/ingest-queue.ts +2 -2
  134. package/src/sessions/remote-ws.ts +1 -1
  135. package/src/tool/workflow/question.ts +1 -1
  136. package/src/util/abort.ts +1 -1
  137. package/src/util/bom.ts +2 -2
  138. package/src/util/color.ts +1 -1
  139. package/src/util/data-url.ts +1 -1
  140. package/src/util/defer.ts +1 -1
  141. package/src/util/error.ts +2 -2
  142. package/src/util/filesystem.ts +2 -2
  143. package/src/util/format.ts +1 -1
  144. package/src/util/iife.ts +1 -1
  145. package/src/util/local-context.ts +1 -1
  146. package/src/util/locale.ts +2 -2
  147. package/src/util/lock.ts +1 -1
  148. package/src/util/network.ts +1 -1
  149. package/src/util/signal.ts +1 -1
  150. package/src/util/token.ts +1 -1
  151. package/src/util/wildcard.ts +1 -1
  152. package/sst-env.d.ts +10 -0
  153. package/test/AGENTS.md +133 -0
  154. package/test/account/repo.test.ts +352 -0
  155. package/test/account/service.test.ts +456 -0
  156. package/test/acp/agent-interface.test.ts +51 -0
  157. package/test/acp/event-subscription.test.ts +725 -0
  158. package/test/agent/agent.test.ts +890 -0
  159. package/test/auth/auth.test.ts +86 -0
  160. package/test/bun/registry.test.ts +75 -0
  161. package/test/bus/bus-effect.test.ts +161 -0
  162. package/test/bus/bus-integration.test.ts +87 -0
  163. package/test/bus/bus.test.ts +219 -0
  164. package/test/cli/account.test.ts +26 -0
  165. package/test/cli/auto-mode.test.ts +75 -0
  166. package/test/cli/bin-saeeol.test.ts +8 -0
  167. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  168. package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
  169. package/test/cli/cmd/tui/sync.test.tsx +159 -0
  170. package/test/cli/error.test.ts +18 -0
  171. package/test/cli/github-action.test.ts +198 -0
  172. package/test/cli/github-remote.test.ts +85 -0
  173. package/test/cli/import.test.ts +97 -0
  174. package/test/cli/install-artifact.test.ts +72 -0
  175. package/test/cli/plugin-auth-picker.test.ts +120 -0
  176. package/test/cli/pr.test.ts +59 -0
  177. package/test/cli/tui/editor-context-zed.test.ts +356 -0
  178. package/test/cli/tui/editor-context.test.tsx +228 -0
  179. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  180. package/test/cli/tui/markdown.test.ts +161 -0
  181. package/test/cli/tui/plugin-add.test.ts +111 -0
  182. package/test/cli/tui/plugin-install.test.ts +87 -0
  183. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  184. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  185. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  186. package/test/cli/tui/plugin-loader.test.ts +816 -0
  187. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  188. package/test/cli/tui/revert-diff.test.ts +35 -0
  189. package/test/cli/tui/slot-replace.test.tsx +47 -0
  190. package/test/cli/tui/theme-store.test.ts +54 -0
  191. package/test/cli/tui/thread.test.ts +28 -0
  192. package/test/cli/tui/transcript.test.ts +426 -0
  193. package/test/cli/tui/usage.test.ts +60 -0
  194. package/test/cli/tui/use-event.test.tsx +175 -0
  195. package/test/config/agent-color.test.ts +67 -0
  196. package/test/config/config.test.ts +2544 -0
  197. package/test/config/fixtures/empty-frontmatter.md +4 -0
  198. package/test/config/fixtures/frontmatter.md +28 -0
  199. package/test/config/fixtures/markdown-header.md +11 -0
  200. package/test/config/fixtures/no-frontmatter.md +1 -0
  201. package/test/config/fixtures/weird-model-id.md +13 -0
  202. package/test/config/lsp.test.ts +87 -0
  203. package/test/config/markdown.test.ts +228 -0
  204. package/test/config/plugin.test.ts +0 -0
  205. package/test/config/tui.test.ts +624 -0
  206. package/test/control-plane/adapters.test.ts +71 -0
  207. package/test/control-plane/workspace.test.ts +1526 -0
  208. package/test/effect/app-runtime-logger.test.ts +98 -0
  209. package/test/effect/config-service.test.ts +65 -0
  210. package/test/effect/instance-state.test.ts +394 -0
  211. package/test/effect/run-service.test.ts +89 -0
  212. package/test/effect/runner.test.ts +523 -0
  213. package/test/fake/provider.ts +82 -0
  214. package/test/file/fsmonitor.test.ts +68 -0
  215. package/test/file/ignore.test.ts +10 -0
  216. package/test/file/index.test.ts +954 -0
  217. package/test/file/path-traversal.test.ts +205 -0
  218. package/test/file/ripgrep.test.ts +226 -0
  219. package/test/file/watcher.test.ts +249 -0
  220. package/test/filesystem/filesystem.test.ts +319 -0
  221. package/test/fixture/db.ts +11 -0
  222. package/test/fixture/fixture.test.ts +26 -0
  223. package/test/fixture/fixture.ts +175 -0
  224. package/test/fixture/flock-worker.ts +72 -0
  225. package/test/fixture/log-init-worker.ts +62 -0
  226. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  227. package/test/fixture/plug-worker.ts +93 -0
  228. package/test/fixture/plugin-meta-worker.ts +19 -0
  229. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  230. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  231. package/test/fixture/skills/index.json +6 -0
  232. package/test/fixture/tui-plugin.ts +323 -0
  233. package/test/fixture/tui-runtime.ts +31 -0
  234. package/test/format/format.test.ts +272 -0
  235. package/test/git/git.test.ts +128 -0
  236. package/test/ide/ide.test.ts +82 -0
  237. package/test/installation/installation.test.ts +168 -0
  238. package/test/keybind.test.ts +421 -0
  239. package/test/lib/effect.ts +53 -0
  240. package/test/lib/filesystem.ts +10 -0
  241. package/test/lib/llm-server.ts +778 -0
  242. package/test/lib/websocket.ts +46 -0
  243. package/test/lsp/client.test.ts +482 -0
  244. package/test/lsp/index.test.ts +160 -0
  245. package/test/lsp/launch.test.ts +22 -0
  246. package/test/lsp/lifecycle.test.ts +184 -0
  247. package/test/ltm/ltm.test.ts +230 -0
  248. package/test/mcp/headers.test.ts +178 -0
  249. package/test/mcp/lifecycle.test.ts +787 -0
  250. package/test/mcp/oauth-auto-connect.test.ts +311 -0
  251. package/test/mcp/oauth-browser.test.ts +276 -0
  252. package/test/mcp/oauth-callback.test.ts +34 -0
  253. package/test/memory/abort-leak-webfetch.ts +49 -0
  254. package/test/memory/abort-leak.test.ts +128 -0
  255. package/test/patch/patch.test.ts +348 -0
  256. package/test/permission/arity.test.ts +33 -0
  257. package/test/permission/next.test.ts +1227 -0
  258. package/test/permission/next.toConfig.test.ts +110 -0
  259. package/test/permission-task.test.ts +326 -0
  260. package/test/plugin/auth-override.test.ts +79 -0
  261. package/test/plugin/cloudflare.test.ts +68 -0
  262. package/test/plugin/codex.test.ts +123 -0
  263. package/test/plugin/github-copilot-models.test.ts +261 -0
  264. package/test/plugin/install-concurrency.test.ts +140 -0
  265. package/test/plugin/install.test.ts +570 -0
  266. package/test/plugin/loader-shared.test.ts +1169 -0
  267. package/test/plugin/meta.test.ts +137 -0
  268. package/test/plugin/plugin-contract.test.ts +291 -0
  269. package/test/plugin/shared.test.ts +88 -0
  270. package/test/plugin/trigger.test.ts +102 -0
  271. package/test/plugin/workspace-adapter.test.ts +109 -0
  272. package/test/preload.ts +77 -0
  273. package/test/project/instance.test.ts +276 -0
  274. package/test/project/migrate-global.test.ts +152 -0
  275. package/test/project/project.test.ts +600 -0
  276. package/test/project/vcs.test.ts +286 -0
  277. package/test/project/worktree-remove.test.ts +126 -0
  278. package/test/project/worktree.test.ts +223 -0
  279. package/test/provider/amazon-bedrock.test.ts +462 -0
  280. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  281. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  282. package/test/provider/gitlab-duo.test.ts +413 -0
  283. package/test/provider/local.test.ts +208 -0
  284. package/test/provider/models.test.ts +261 -0
  285. package/test/provider/provider-category.test.ts +190 -0
  286. package/test/provider/provider.test.ts +2758 -0
  287. package/test/provider/transform.test.ts +3681 -0
  288. package/test/pty/pty-output-isolation.test.ts +147 -0
  289. package/test/pty/pty-session.test.ts +102 -0
  290. package/test/pty/pty-shell.test.ts +104 -0
  291. package/test/question/question.test.ts +490 -0
  292. package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
  293. package/test/saeeol/agent-manager-tool.test.ts +71 -0
  294. package/test/saeeol/agent-permission-overrides.test.ts +75 -0
  295. package/test/saeeol/agent-skill-permissions.test.ts +37 -0
  296. package/test/saeeol/ask-agent-permissions.test.ts +303 -0
  297. package/test/saeeol/bash-hierarchy.test.ts +64 -0
  298. package/test/saeeol/bash-permission-metadata.test.ts +66 -0
  299. package/test/saeeol/bash-security-extended.test.ts +243 -0
  300. package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
  301. package/test/saeeol/boxes-integration.test.ts +415 -0
  302. package/test/saeeol/builtin-skills.test.ts +75 -0
  303. package/test/saeeol/cleanup.ts +28 -0
  304. package/test/saeeol/cli/dev-setup.test.ts +74 -0
  305. package/test/saeeol/cli/roll-call.test.ts +161 -0
  306. package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
  307. package/test/saeeol/codex-auth-refresh.test.ts +124 -0
  308. package/test/saeeol/commit-message/generate.test.ts +188 -0
  309. package/test/saeeol/commit-message/git-context.test.ts +303 -0
  310. package/test/saeeol/commit-message-windows.test.ts +38 -0
  311. package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
  312. package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
  313. package/test/saeeol/compaction-skip-guard.test.ts +224 -0
  314. package/test/saeeol/compaction-smart-select.test.ts +100 -0
  315. package/test/saeeol/config/config.test.ts +166 -0
  316. package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
  317. package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
  318. package/test/saeeol/config-gitignore.test.ts +70 -0
  319. package/test/saeeol/config-injector.test.ts +305 -0
  320. package/test/saeeol/config-resilience.test.ts +234 -0
  321. package/test/saeeol/config-validation.test.ts +183 -0
  322. package/test/saeeol/cost-propagation.test.ts +94 -0
  323. package/test/saeeol/cost-tracker-extended.test.ts +141 -0
  324. package/test/saeeol/cost-tracker.test.ts +64 -0
  325. package/test/saeeol/custom-provider-delete.test.ts +149 -0
  326. package/test/saeeol/diff-full.test.ts +226 -0
  327. package/test/saeeol/edit-permission-filediff.test.ts +223 -0
  328. package/test/saeeol/encoding.test.ts +364 -0
  329. package/test/saeeol/enhance-prompt.test.ts +61 -0
  330. package/test/saeeol/ensure-plan-dir.test.ts +32 -0
  331. package/test/saeeol/errors.test.ts +144 -0
  332. package/test/saeeol/external-directory-boundary.test.ts +96 -0
  333. package/test/saeeol/gateway-headers.test.ts +88 -0
  334. package/test/saeeol/help.test.ts +191 -0
  335. package/test/saeeol/ignore-migrator.test.ts +308 -0
  336. package/test/saeeol/indexing-auth.test.ts +45 -0
  337. package/test/saeeol/indexing-feature.test.ts +44 -0
  338. package/test/saeeol/indexing-label.test.ts +70 -0
  339. package/test/saeeol/indexing-startup.test.ts +381 -0
  340. package/test/saeeol/indexing-worktree.test.ts +73 -0
  341. package/test/saeeol/instruction.test.ts +136 -0
  342. package/test/saeeol/lancedb-runtime.test.ts +116 -0
  343. package/test/saeeol/loader-auth.test.ts +168 -0
  344. package/test/saeeol/local-model.test.ts +621 -0
  345. package/test/saeeol/logo.test.ts +31 -0
  346. package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
  347. package/test/saeeol/mcp-branding.test.ts +33 -0
  348. package/test/saeeol/mcp-docker-rm.test.ts +32 -0
  349. package/test/saeeol/mcp-migrator.test.ts +736 -0
  350. package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
  351. package/test/saeeol/memory-io.test.ts +198 -0
  352. package/test/saeeol/memory-paths.test.ts +87 -0
  353. package/test/saeeol/memory-security.test.ts +166 -0
  354. package/test/saeeol/model-cache-org.test.ts +164 -0
  355. package/test/saeeol/model-info-panel-utils.test.ts +52 -0
  356. package/test/saeeol/model-info-panel.types.test.ts +7 -0
  357. package/test/saeeol/models-401-fallback.test.ts +52 -0
  358. package/test/saeeol/modes-migrator.test.ts +320 -0
  359. package/test/saeeol/nvidia-headers.test.ts +74 -0
  360. package/test/saeeol/patch-jsonc.test.ts +73 -0
  361. package/test/saeeol/patch.test.ts +172 -0
  362. package/test/saeeol/paths.test.ts +265 -0
  363. package/test/saeeol/permission/config-paths.test.ts +174 -0
  364. package/test/saeeol/permission/env-read.test.ts +149 -0
  365. package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
  366. package/test/saeeol/permission/next.always-rules.test.ts +882 -0
  367. package/test/saeeol/permission/next.reply-http.test.ts +205 -0
  368. package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
  369. package/test/saeeol/plan-exit-detection.test.ts +494 -0
  370. package/test/saeeol/plan-followup.test.ts +1376 -0
  371. package/test/saeeol/project-config-update.test.ts +120 -0
  372. package/test/saeeol/project-id.test.ts +455 -0
  373. package/test/saeeol/provider-cost.test.ts +171 -0
  374. package/test/saeeol/provider-list-failed-state.test.ts +100 -0
  375. package/test/saeeol/question-dismiss-all.test.ts +174 -0
  376. package/test/saeeol/read-directory.test.ts +116 -0
  377. package/test/saeeol/rules-migrator.test.ts +257 -0
  378. package/test/saeeol/run-auto.test.ts +176 -0
  379. package/test/saeeol/run-network.test.ts +224 -0
  380. package/test/saeeol/semantic-search.test.ts +186 -0
  381. package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
  382. package/test/saeeol/session/instruction-substitution.test.ts +72 -0
  383. package/test/saeeol/session/platform-attribution.test.ts +118 -0
  384. package/test/saeeol/session/session.test.ts +105 -0
  385. package/test/saeeol/session-compaction-cap.test.ts +399 -0
  386. package/test/saeeol/session-compaction-chunks.test.ts +501 -0
  387. package/test/saeeol/session-compaction-safety.test.ts +481 -0
  388. package/test/saeeol/session-fork-remap.test.ts +251 -0
  389. package/test/saeeol/session-import-service.test.ts +114 -0
  390. package/test/saeeol/session-list.test.ts +47 -0
  391. package/test/saeeol/session-message-metadata.test.ts +128 -0
  392. package/test/saeeol/session-overflow.test.ts +78 -0
  393. package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
  394. package/test/saeeol/session-processor-network-offline.test.ts +204 -0
  395. package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
  396. package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
  397. package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
  398. package/test/saeeol/session-prompt-queue.test.ts +815 -0
  399. package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
  400. package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
  401. package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
  402. package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
  403. package/test/saeeol/sessions/remote-ws.test.ts +367 -0
  404. package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
  405. package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
  406. package/test/saeeol/snapshot-cache.test.ts +84 -0
  407. package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
  408. package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
  409. package/test/saeeol/stats-subagent-cost.test.ts +123 -0
  410. package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
  411. package/test/saeeol/suggestion/suggestion.test.ts +145 -0
  412. package/test/saeeol/suggestion/tool.test.ts +298 -0
  413. package/test/saeeol/summary-file-diff.test.ts +28 -0
  414. package/test/saeeol/system-prompt.test.ts +142 -0
  415. package/test/saeeol/task-nesting.test.ts +193 -0
  416. package/test/saeeol/telemetry/feedback.test.ts +8 -0
  417. package/test/saeeol/todo-view.test.ts +57 -0
  418. package/test/saeeol/tool-encoding.test.ts +455 -0
  419. package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
  420. package/test/saeeol/tool-registry-indexing.test.ts +236 -0
  421. package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
  422. package/test/saeeol/tool-task-model.test.ts +352 -0
  423. package/test/saeeol/transform-opus-4.7.test.ts +89 -0
  424. package/test/saeeol/tui-diff.test.ts +91 -0
  425. package/test/saeeol/tui-sync.test.ts +80 -0
  426. package/test/saeeol/util/url.test.ts +141 -0
  427. package/test/saeeol/workflows-migrator.test.ts +261 -0
  428. package/test/saeeol/worktree-diff-summary.test.ts +64 -0
  429. package/test/saeeol/worktree-diff.test.ts +223 -0
  430. package/test/saeeol/worktree-remove-lock.test.ts +82 -0
  431. package/test/server/AGENTS.md +15 -0
  432. package/test/server/contract.test.ts +357 -0
  433. package/test/server/experimental-session-list.test.ts +157 -0
  434. package/test/server/global-session-list.test.ts +155 -0
  435. package/test/server/httpapi-authorization.test.ts +103 -0
  436. package/test/server/httpapi-bridge.test.ts +440 -0
  437. package/test/server/httpapi-config.test.ts +67 -0
  438. package/test/server/httpapi-cors.test.ts +89 -0
  439. package/test/server/httpapi-event.test.ts +57 -0
  440. package/test/server/httpapi-experimental.test.ts +219 -0
  441. package/test/server/httpapi-file.test.ts +79 -0
  442. package/test/server/httpapi-instance-context.test.ts +237 -0
  443. package/test/server/httpapi-instance.legacy.test.ts +140 -0
  444. package/test/server/httpapi-instance.test.ts +83 -0
  445. package/test/server/httpapi-json-parity.test.ts +263 -0
  446. package/test/server/httpapi-mcp-oauth.test.ts +76 -0
  447. package/test/server/httpapi-mcp.test.ts +189 -0
  448. package/test/server/httpapi-provider.test.ts +153 -0
  449. package/test/server/httpapi-pty-websocket.test.ts +16 -0
  450. package/test/server/httpapi-pty.test.ts +175 -0
  451. package/test/server/httpapi-raw-route-auth.test.ts +89 -0
  452. package/test/server/httpapi-sdk.test.ts +681 -0
  453. package/test/server/httpapi-session.test.ts +464 -0
  454. package/test/server/httpapi-sync.test.ts +130 -0
  455. package/test/server/httpapi-tui.test.ts +121 -0
  456. package/test/server/httpapi-workspace-routing.test.ts +471 -0
  457. package/test/server/httpapi-workspace.test.ts +427 -0
  458. package/test/server/lib/conformance.ts +88 -0
  459. package/test/server/lib/stateful.ts +112 -0
  460. package/test/server/project-init-git.test.ts +113 -0
  461. package/test/server/proxy-util.test.ts +113 -0
  462. package/test/server/session-actions.test.ts +49 -0
  463. package/test/server/session-list.test.ts +238 -0
  464. package/test/server/session-messages.test.ts +167 -0
  465. package/test/server/session-select.test.ts +100 -0
  466. package/test/server/trace-attributes.test.ts +76 -0
  467. package/test/server/workspace-proxy.test.ts +165 -0
  468. package/test/server/workspace-routing.test.ts +85 -0
  469. package/test/session/compaction.test.ts +2420 -0
  470. package/test/session/instruction.test.ts +247 -0
  471. package/test/session/llm.test.ts +1273 -0
  472. package/test/session/message-v2.test.ts +1291 -0
  473. package/test/session/messages-pagination.test.ts +1173 -0
  474. package/test/session/network.test.ts +249 -0
  475. package/test/session/processor-effect.test.ts +847 -0
  476. package/test/session/prompt.test.ts +2131 -0
  477. package/test/session/retry.test.ts +340 -0
  478. package/test/session/revert-compact.test.ts +639 -0
  479. package/test/session/schema-decoding.test.ts +311 -0
  480. package/test/session/session-entry-stepper.test.ts +917 -0
  481. package/test/session/session-schema.test.ts +76 -0
  482. package/test/session/snapshot-tool-race.test.ts +257 -0
  483. package/test/session/structured-output-integration.test.ts +265 -0
  484. package/test/session/structured-output.test.ts +381 -0
  485. package/test/session/system.test.ts +73 -0
  486. package/test/share/share-next.test.ts +333 -0
  487. package/test/shell/shell.test.ts +99 -0
  488. package/test/skill/discovery.test.ts +116 -0
  489. package/test/skill/skill.test.ts +393 -0
  490. package/test/snapshot/snapshot.test.ts +1531 -0
  491. package/test/storage/db.test.ts +23 -0
  492. package/test/storage/json-migration.test.ts +832 -0
  493. package/test/storage/storage.test.ts +293 -0
  494. package/test/suggestion/suggestion.test.ts +1 -0
  495. package/test/sync/index.test.ts +256 -0
  496. package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
  497. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  498. package/test/tool/apply_patch.test.ts +614 -0
  499. package/test/tool/bash.test.ts +1225 -0
  500. package/test/tool/diagnostics-filter.test.ts +55 -0
  501. package/test/tool/edit.test.ts +754 -0
  502. package/test/tool/external-directory.test.ts +169 -0
  503. package/test/tool/fixtures/large-image.png +0 -0
  504. package/test/tool/fixtures/models-api.json +65179 -0
  505. package/test/tool/glob.test.ts +107 -0
  506. package/test/tool/grep.test.ts +114 -0
  507. package/test/tool/lsp.test.ts +187 -0
  508. package/test/tool/parameters.test.ts +243 -0
  509. package/test/tool/question.test.ts +129 -0
  510. package/test/tool/read.test.ts +500 -0
  511. package/test/tool/recall.test.ts +151 -0
  512. package/test/tool/registry.test.ts +203 -0
  513. package/test/tool/skill.test.ts +135 -0
  514. package/test/tool/suggest.test.ts +1 -0
  515. package/test/tool/task.test.ts +612 -0
  516. package/test/tool/tool-define.test.ts +99 -0
  517. package/test/tool/truncation.test.ts +260 -0
  518. package/test/tool/webfetch.test.ts +103 -0
  519. package/test/tool/write.test.ts +291 -0
  520. package/test/util/data-url.test.ts +14 -0
  521. package/test/util/effect-zod.test.ts +754 -0
  522. package/test/util/error.test.ts +38 -0
  523. package/test/util/filesystem.test.ts +656 -0
  524. package/test/util/format.test.ts +59 -0
  525. package/test/util/glob.test.ts +164 -0
  526. package/test/util/iife.test.ts +36 -0
  527. package/test/util/lazy.test.ts +50 -0
  528. package/test/util/lock.test.ts +72 -0
  529. package/test/util/log.test.ts +86 -0
  530. package/test/util/module.test.ts +59 -0
  531. package/test/util/process.test.ts +128 -0
  532. package/test/util/timeout.test.ts +21 -0
  533. package/test/util/which.test.ts +100 -0
  534. package/test/util/wildcard.test.ts +90 -0
  535. package/test/workspace/workspace-restore.test.ts +296 -0
  536. package/src/provider/models-snapshot.d.ts +0 -2
  537. package/src/provider/models-snapshot.js +0 -3
@@ -0,0 +1,52 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { avgPrice, fmtCachedPrice, fmtContext, fmtPrice } from "../../src/overlay/components/model-info-panel-utils"
3
+
4
+ describe("model info panel price formatting", () => {
5
+ test("fmtPrice returns Free for zero", () => {
6
+ expect(fmtPrice(0)).toBe("Free")
7
+ })
8
+
9
+ test("fmtPrice uses four decimals for very small prices", () => {
10
+ expect(fmtPrice(0.0095)).toBe("$0.0095/1M")
11
+ })
12
+
13
+ test("fmtPrice uses two decimals for standard prices", () => {
14
+ expect(fmtPrice(3)).toBe("$3.00/1M")
15
+ })
16
+
17
+ test("fmtCachedPrice returns cache read price when available", () => {
18
+ expect(fmtCachedPrice({ input: 3, output: 15, cache: { read: 0.3, write: 0 } })).toBe("$0.30/1M")
19
+ })
20
+
21
+ test("fmtCachedPrice returns Free for free models", () => {
22
+ expect(fmtCachedPrice({ input: 0, output: 0, cache: { read: 0, write: 0 } })).toBe("Free")
23
+ })
24
+
25
+ test("fmtCachedPrice returns null without cache read", () => {
26
+ expect(fmtCachedPrice({ input: 3, output: 15, cache: { read: 0, write: 0 } })).toBeNull()
27
+ })
28
+
29
+ test("avgPrice uses cache weighted formula when cache read exists", () => {
30
+ const val = avgPrice({ input: 3, output: 15, cache: { read: 0.3, write: 0 } })
31
+ expect(val).toBe(2.31)
32
+ })
33
+
34
+ test("avgPrice uses input and output weighted formula without cache read", () => {
35
+ const val = avgPrice({ input: 3, output: 15, cache: { read: 0, write: 0 } })
36
+ expect(val).toBe(4.2)
37
+ })
38
+ })
39
+
40
+ describe("model info panel context formatting", () => {
41
+ test("formats thousands as K", () => {
42
+ expect(fmtContext(128000)).toBe("128K")
43
+ })
44
+
45
+ test("formats millions as M", () => {
46
+ expect(fmtContext(1000000)).toBe("1M")
47
+ })
48
+
49
+ test("returns exact value for small contexts", () => {
50
+ expect(fmtContext(800)).toBe("800")
51
+ })
52
+ })
@@ -0,0 +1,7 @@
1
+ import type { Model as SDKModel } from "@saeeol/sdk/v2"
2
+ import { ModelInfoPanel } from "@/saeeol/components/model-info-panel"
3
+
4
+ type Assert<T extends true> = T
5
+ type Props = Parameters<typeof ModelInfoPanel>[0]
6
+
7
+ type _SyncModelMatchesPanel = Assert<SDKModel extends Props["model"] ? true : false>
@@ -0,0 +1,52 @@
1
+ // Integration: when fetchSaeeolModels returns a 401 error result, ModelCache
2
+ // surfaces the failure and caches empty models (allowing re-auth via /connect).
3
+ // The real 401-fallback unit test lives in packages/gateway/test/api/models.test.ts.
4
+
5
+ import { test, expect, mock } from "bun:test"
6
+ import path from "path"
7
+ import * as Log from "@saeeol/core/util/log"
8
+
9
+ Log.init({ print: false })
10
+
11
+ // Simulate a 401 typed error result from the gateway
12
+ mock.module("@saeeol/gateway", () => ({
13
+ fetchSaeeolModels: async () => ({
14
+ models: {},
15
+ error: { kind: "unauthorized", status: 401 },
16
+ }),
17
+ SAEEOL_OPENROUTER_BASE: "https://api.saeeol.ai/api/openrouter",
18
+ }))
19
+
20
+ mock.module("saeeol-copilot-auth", () => ({ default: () => ({}) }))
21
+ mock.module("saeeol-anthropic-auth", () => ({ default: () => ({}) }))
22
+ mock.module("@gitlab/saeeol-gitlab-auth", () => ({ default: () => ({}) }))
23
+
24
+ import { tmpdir } from "../fixture/fixture"
25
+ import { Instance } from "../../src/project/instance"
26
+ import { ModelCache } from "../../src/provider/model-cache"
27
+
28
+ const CONFIG = JSON.stringify({ $schema: "https://app.saeeol.ai/config.json" })
29
+
30
+ async function withInstance<T>(fn: () => Promise<T>): Promise<T> {
31
+ await using tmp = await tmpdir({
32
+ init: async (dir) => {
33
+ await Bun.write(path.join(dir, "saeeol.json"), CONFIG)
34
+ },
35
+ })
36
+ return Instance.provide({ directory: tmp.path, fn })
37
+ }
38
+
39
+ test("401 from gateway sets provider as failed in ModelCache", async () => {
40
+ ModelCache.clear("saeeol")
41
+ await withInstance(() => ModelCache.fetch("saeeol"))
42
+ expect(ModelCache.failedProviders()).toContain("saeeol")
43
+ expect(ModelCache.getFailure("saeeol")).toMatchObject({ kind: "unauthorized", status: 401 })
44
+ })
45
+
46
+ test("401 from gateway caches empty models (not undefined)", async () => {
47
+ ModelCache.clear("saeeol")
48
+ await withInstance(() => ModelCache.fetch("saeeol"))
49
+ const cached = ModelCache.get("saeeol")
50
+ expect(cached).toBeDefined()
51
+ expect(Object.keys(cached!)).toHaveLength(0)
52
+ })
@@ -0,0 +1,320 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { ModesMigrator } from "../../src/overlay/modes-migrator"
3
+ import { tmpdir } from "../fixture/fixture"
4
+ import path from "path"
5
+
6
+ describe("ModesMigrator", () => {
7
+ describe("isDefaultMode", () => {
8
+ test("returns true for default modes", () => {
9
+ expect(ModesMigrator.isDefaultMode("code")).toBe(true)
10
+ expect(ModesMigrator.isDefaultMode("architect")).toBe(true)
11
+ expect(ModesMigrator.isDefaultMode("ask")).toBe(true)
12
+ expect(ModesMigrator.isDefaultMode("debug")).toBe(true)
13
+ expect(ModesMigrator.isDefaultMode("orchestrator")).toBe(true)
14
+ })
15
+
16
+ test("returns false for custom modes", () => {
17
+ expect(ModesMigrator.isDefaultMode("translate")).toBe(false)
18
+ expect(ModesMigrator.isDefaultMode("my-custom-mode")).toBe(false)
19
+ expect(ModesMigrator.isDefaultMode("reviewer")).toBe(false)
20
+ })
21
+ })
22
+
23
+ describe("convertPermissions", () => {
24
+ test("converts simple groups to permissions and denies missing", () => {
25
+ const groups = ["read", "edit", "command"]
26
+ const permissions = ModesMigrator.convertPermissions(groups)
27
+
28
+ expect(permissions.read).toBe("allow")
29
+ expect(permissions.edit).toBe("allow")
30
+ expect(permissions.bash).toBe("allow")
31
+ expect(permissions.mcp).toBe("deny") // Not in groups, should be denied
32
+ })
33
+
34
+ test("maps browser group to bash permission", () => {
35
+ const groups = ["browser"]
36
+ const permissions = ModesMigrator.convertPermissions(groups)
37
+
38
+ expect(permissions.bash).toBe("allow")
39
+ expect(permissions.read).toBe("deny")
40
+ expect(permissions.edit).toBe("deny")
41
+ expect(permissions.mcp).toBe("deny")
42
+ })
43
+
44
+ test("maps mcp group to mcp permission", () => {
45
+ const groups = ["mcp"]
46
+ const permissions = ModesMigrator.convertPermissions(groups)
47
+
48
+ expect(permissions.mcp).toBe("allow")
49
+ expect(permissions.read).toBe("deny")
50
+ expect(permissions.edit).toBe("deny")
51
+ expect(permissions.bash).toBe("deny")
52
+ })
53
+
54
+ test("converts fileRegex groups to restricted permissions", () => {
55
+ const groups: ModesMigrator.SaeeolMode["groups"] = [
56
+ "read",
57
+ ["edit", { fileRegex: "\\.md$", description: "Markdown only" }],
58
+ ]
59
+ const permissions = ModesMigrator.convertPermissions(groups)
60
+
61
+ expect(permissions.read).toBe("allow")
62
+ expect(permissions.edit).toEqual({
63
+ "\\.md$": "allow",
64
+ "*": "deny",
65
+ })
66
+ expect(permissions.bash).toBe("deny")
67
+ expect(permissions.mcp).toBe("deny")
68
+ })
69
+
70
+ test("handles tuple without fileRegex", () => {
71
+ const groups: ModesMigrator.SaeeolMode["groups"] = [["edit", {}]]
72
+ const permissions = ModesMigrator.convertPermissions(groups)
73
+
74
+ expect(permissions.edit).toBe("allow")
75
+ expect(permissions.read).toBe("deny")
76
+ expect(permissions.bash).toBe("deny")
77
+ expect(permissions.mcp).toBe("deny")
78
+ })
79
+
80
+ test("passes through unknown groups but still denies standard permissions", () => {
81
+ const groups = ["custom-group"]
82
+ const permissions = ModesMigrator.convertPermissions(groups)
83
+
84
+ expect(permissions["custom-group"]).toBe("allow")
85
+ expect(permissions.read).toBe("deny")
86
+ expect(permissions.edit).toBe("deny")
87
+ expect(permissions.bash).toBe("deny")
88
+ expect(permissions.mcp).toBe("deny")
89
+ })
90
+
91
+ test("denies bash and mcp when only read and edit are allowed", () => {
92
+ const groups = ["read", "edit"]
93
+ const permissions = ModesMigrator.convertPermissions(groups)
94
+
95
+ expect(permissions.read).toBe("allow")
96
+ expect(permissions.edit).toBe("allow")
97
+ expect(permissions.bash).toBe("deny")
98
+ expect(permissions.mcp).toBe("deny")
99
+ })
100
+ })
101
+
102
+ describe("convertMode", () => {
103
+ test("converts full mode to agent config", () => {
104
+ const mode: ModesMigrator.SaeeolMode = {
105
+ slug: "translate",
106
+ name: "Translate",
107
+ roleDefinition: "You are a translator...",
108
+ customInstructions: "Translate accurately.",
109
+ groups: ["read", ["edit", { fileRegex: "\\.json$" }]],
110
+ }
111
+
112
+ const agent = ModesMigrator.convertMode(mode)
113
+
114
+ expect(agent.mode).toBe("primary")
115
+ expect(agent.prompt).toBe("You are a translator...\n\nTranslate accurately.")
116
+ expect(agent.permission?.read).toBe("allow")
117
+ expect(agent.permission?.edit).toEqual({
118
+ "\\.json$": "allow",
119
+ "*": "deny",
120
+ })
121
+ })
122
+
123
+ test("uses description when available", () => {
124
+ const mode: ModesMigrator.SaeeolMode = {
125
+ slug: "test",
126
+ name: "Test",
127
+ roleDefinition: "Role",
128
+ description: "Custom description",
129
+ groups: [],
130
+ }
131
+
132
+ const agent = ModesMigrator.convertMode(mode)
133
+ expect(agent.description).toBe("Custom description")
134
+ })
135
+
136
+ test("falls back to whenToUse for description", () => {
137
+ const mode: ModesMigrator.SaeeolMode = {
138
+ slug: "test",
139
+ name: "Test",
140
+ roleDefinition: "Role",
141
+ whenToUse: "When to use this mode",
142
+ groups: [],
143
+ }
144
+
145
+ const agent = ModesMigrator.convertMode(mode)
146
+ expect(agent.description).toBe("When to use this mode")
147
+ })
148
+
149
+ test("falls back to name for description", () => {
150
+ const mode: ModesMigrator.SaeeolMode = {
151
+ slug: "test",
152
+ name: "Test Mode",
153
+ roleDefinition: "Role",
154
+ groups: [],
155
+ }
156
+
157
+ const agent = ModesMigrator.convertMode(mode)
158
+ expect(agent.description).toBe("Test Mode")
159
+ })
160
+
161
+ test("handles mode without customInstructions", () => {
162
+ const mode: ModesMigrator.SaeeolMode = {
163
+ slug: "test",
164
+ name: "Test",
165
+ roleDefinition: "You are a test agent.",
166
+ groups: ["read"],
167
+ }
168
+
169
+ const agent = ModesMigrator.convertMode(mode)
170
+ expect(agent.prompt).toBe("You are a test agent.")
171
+ })
172
+ })
173
+
174
+ describe("readModesFile", () => {
175
+ test("returns empty array for non-existent file", async () => {
176
+ const modes = await ModesMigrator.readModesFile("/non/existent/path.yaml")
177
+ expect(modes).toEqual([])
178
+ })
179
+
180
+ test("reads and parses yaml file", async () => {
181
+ await using tmp = await tmpdir({
182
+ init: async (dir) => {
183
+ await Bun.write(
184
+ path.join(dir, "modes.yaml"),
185
+ `customModes:
186
+ - slug: translate
187
+ name: Translate
188
+ roleDefinition: You are a translator
189
+ groups:
190
+ - read
191
+ - edit`,
192
+ )
193
+ },
194
+ })
195
+
196
+ const modes = await ModesMigrator.readModesFile(path.join(tmp.path, "modes.yaml"))
197
+ expect(modes).toHaveLength(1)
198
+ expect(modes[0].slug).toBe("translate")
199
+ expect(modes[0].name).toBe("Translate")
200
+ })
201
+
202
+ test("returns empty array for file without customModes", async () => {
203
+ await using tmp = await tmpdir({
204
+ init: async (dir) => {
205
+ await Bun.write(path.join(dir, "modes.yaml"), "someOtherKey: value")
206
+ },
207
+ })
208
+
209
+ const modes = await ModesMigrator.readModesFile(path.join(tmp.path, "modes.yaml"))
210
+ expect(modes).toEqual([])
211
+ })
212
+ })
213
+
214
+ describe("migrate", () => {
215
+ test("skips default modes", async () => {
216
+ await using tmp = await tmpdir({
217
+ init: async (dir) => {
218
+ await Bun.write(
219
+ path.join(dir, ".saeeolmodes"),
220
+ `customModes:
221
+ - slug: code
222
+ name: Code
223
+ roleDefinition: Default code mode
224
+ groups:
225
+ - read
226
+ - edit
227
+ - slug: translate
228
+ name: Translate
229
+ roleDefinition: Custom translator
230
+ groups:
231
+ - read`,
232
+ )
233
+ },
234
+ })
235
+
236
+ const result = await ModesMigrator.migrate({ projectDir: tmp.path, skipGlobalPaths: true })
237
+
238
+ expect(result.skipped).toHaveLength(1)
239
+ expect(result.skipped[0].slug).toBe("code")
240
+ expect(result.agents).toHaveProperty("translate")
241
+ expect(result.agents).not.toHaveProperty("code")
242
+ })
243
+
244
+ test("deduplicates modes by slug with later entries winning", async () => {
245
+ await using tmp = await tmpdir({
246
+ init: async (dir) => {
247
+ // Create global settings dir
248
+ const globalDir = path.join(dir, "global-settings")
249
+ await Bun.write(
250
+ path.join(globalDir, "custom_modes.yaml"),
251
+ `customModes:
252
+ - slug: translate
253
+ name: Translate Global
254
+ roleDefinition: Global translator
255
+ groups:
256
+ - read`,
257
+ )
258
+
259
+ // Create project .saeeolmodes (should win)
260
+ await Bun.write(
261
+ path.join(dir, ".saeeolmodes"),
262
+ `customModes:
263
+ - slug: translate
264
+ name: Translate Project
265
+ roleDefinition: Project translator
266
+ groups:
267
+ - read
268
+ - edit`,
269
+ )
270
+
271
+ return globalDir
272
+ },
273
+ })
274
+
275
+ const result = await ModesMigrator.migrate({
276
+ projectDir: tmp.path,
277
+ globalSettingsDir: tmp.extra,
278
+ })
279
+
280
+ expect(result.agents.translate.prompt).toBe("Project translator")
281
+ })
282
+
283
+ test("returns empty agents when no custom modes exist", async () => {
284
+ await using tmp = await tmpdir()
285
+
286
+ const result = await ModesMigrator.migrate({ projectDir: tmp.path, skipGlobalPaths: true })
287
+
288
+ expect(Object.keys(result.agents)).toHaveLength(0)
289
+ expect(result.skipped).toHaveLength(0)
290
+ })
291
+
292
+ test("migrates multiple custom modes", async () => {
293
+ await using tmp = await tmpdir({
294
+ init: async (dir) => {
295
+ await Bun.write(
296
+ path.join(dir, ".saeeolmodes"),
297
+ `customModes:
298
+ - slug: translate
299
+ name: Translate
300
+ roleDefinition: Translator
301
+ groups:
302
+ - read
303
+ - slug: reviewer
304
+ name: Reviewer
305
+ roleDefinition: Code reviewer
306
+ groups:
307
+ - read
308
+ - edit`,
309
+ )
310
+ },
311
+ })
312
+
313
+ const result = await ModesMigrator.migrate({ projectDir: tmp.path, skipGlobalPaths: true })
314
+
315
+ expect(Object.keys(result.agents)).toHaveLength(2)
316
+ expect(result.agents).toHaveProperty("translate")
317
+ expect(result.agents).toHaveProperty("reviewer")
318
+ })
319
+ })
320
+ })
@@ -0,0 +1,74 @@
1
+ import { expect } from "bun:test"
2
+ import path from "path"
3
+ import { Effect, Layer } from "effect"
4
+ import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
5
+ import { provideTmpdirInstance } from "../fixture/fixture"
6
+ import { testEffect } from "../lib/effect"
7
+ import { Env } from "../../src/env"
8
+ import { Provider } from "../../src/provider/provider"
9
+ import { ProviderID } from "../../src/provider/schema"
10
+
11
+ const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, CrossSpawnSpawner.defaultLayer))
12
+
13
+ function withNvidiaKey<A, E, R>(self: Effect.Effect<A, E, R>) {
14
+ return Effect.gen(function* () {
15
+ const env = yield* Env.Service
16
+ yield* env.set("NVIDIA_API_KEY", "test-api-key")
17
+ yield* Effect.addFinalizer(() => env.remove("NVIDIA_API_KEY"))
18
+ return yield* self
19
+ })
20
+ }
21
+
22
+ it.live("nvidia provider includes saeeol billing origin header", () =>
23
+ provideTmpdirInstance(() =>
24
+ withNvidiaKey(
25
+ Provider.Service.use((provider) =>
26
+ Effect.gen(function* () {
27
+ const providers = yield* provider.list()
28
+ const headers = providers[ProviderID.make("nvidia")].options.headers
29
+
30
+ expect(headers["HTTP-Referer"]).toBe("https://saeeol.ai/")
31
+ expect(headers["X-Title"]).toBe("SAEEOL")
32
+ expect(headers["X-BILLING-INVOKE-ORIGIN"]).toBe("saeeol")
33
+ }),
34
+ ),
35
+ ),
36
+ ),
37
+ )
38
+
39
+ it.live("nvidia billing origin header can be overridden from config", () =>
40
+ provideTmpdirInstance((dir) =>
41
+ Effect.gen(function* () {
42
+ yield* Effect.promise(() =>
43
+ Bun.write(
44
+ path.join(dir, "saeeol.json"),
45
+ JSON.stringify({
46
+ $schema: "https://app.saeeol.ai/config.json",
47
+ provider: {
48
+ nvidia: {
49
+ options: {
50
+ headers: {
51
+ "X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
52
+ },
53
+ },
54
+ },
55
+ },
56
+ }),
57
+ ),
58
+ )
59
+
60
+ return yield* withNvidiaKey(
61
+ Provider.Service.use((provider) =>
62
+ Effect.gen(function* () {
63
+ const providers = yield* provider.list()
64
+ const headers = providers[ProviderID.make("nvidia")].options.headers
65
+
66
+ expect(headers["HTTP-Referer"]).toBe("https://saeeol.ai/")
67
+ expect(headers["X-Title"]).toBe("SAEEOL")
68
+ expect(headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin")
69
+ }),
70
+ ),
71
+ )
72
+ }),
73
+ ),
74
+ )
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { applyEdits, modify, findNodeAtLocation, parseTree } from "jsonc-parser"
3
+
4
+ // Replicate patchJsonc logic locally to test it in isolation
5
+ function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return !!value && typeof value === "object" && !Array.isArray(value)
7
+ }
8
+
9
+ function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
10
+ if (!isRecord(patch)) {
11
+ const edits = modify(input, path, patch === null ? undefined : patch, {
12
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
13
+ })
14
+ return applyEdits(input, edits)
15
+ }
16
+
17
+ if (path.length > 0) {
18
+ const tree = parseTree(input)
19
+ const node = tree && findNodeAtLocation(tree, path)
20
+ if (node && node.type !== "object") {
21
+ const edits = modify(input, path, patch, {
22
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
23
+ })
24
+ return applyEdits(input, edits)
25
+ }
26
+ }
27
+
28
+ return Object.entries(patch).reduce((result, [key, value]) => {
29
+ if (value === undefined) return result
30
+ return patchJsonc(result, value, [...path, key])
31
+ }, input)
32
+ }
33
+
34
+ describe("patchJsonc scalar-to-object transition", () => {
35
+ test("sets object when node does not yet exist", () => {
36
+ const input = `{ "permission": {} }`
37
+ const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } })
38
+ const parsed = JSON.parse(result)
39
+ expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" })
40
+ })
41
+
42
+ test("transitions permission rule from string to object (the bug scenario)", () => {
43
+ // bash is stored as a plain string "ask" in the JSONC file
44
+ const input = `{ "permission": { "bash": "ask" } }`
45
+ // User adds exception "uname" → "allow"; this would previously throw:
46
+ // "Can not add index to parent of type string"
47
+ const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } })
48
+ const parsed = JSON.parse(result)
49
+ expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" })
50
+ })
51
+
52
+ test("does not disturb sibling keys when replacing scalar", () => {
53
+ const input = `{ "permission": { "bash": "ask", "glob": "allow" } }`
54
+ const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } })
55
+ const parsed = JSON.parse(result)
56
+ expect(parsed.permission.glob).toBe("allow")
57
+ expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" })
58
+ })
59
+
60
+ test("updates existing object permission by adding a new key", () => {
61
+ const input = `{ "permission": { "bash": { "*": "ask" } } }`
62
+ const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } })
63
+ const parsed = JSON.parse(result)
64
+ expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" })
65
+ })
66
+
67
+ test("plain string permission update still works", () => {
68
+ const input = `{ "permission": { "bash": "allow" } }`
69
+ const result = patchJsonc(input, { permission: { bash: "ask" } })
70
+ const parsed = JSON.parse(result)
71
+ expect(parsed.permission.bash).toBe("ask")
72
+ })
73
+ })