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,455 @@
1
+ // Integration tests verifying that the agent file tools (read, write, edit,
2
+ // apply_patch) detect and preserve the original encoding of files on disk.
3
+ // Tests exercise the real tool pipeline rather than the Encoding helper
4
+ // directly so we validate end-to-end behaviour.
5
+
6
+ import { afterEach, describe, expect } from "bun:test"
7
+ import { Effect, Layer } from "effect"
8
+ import path from "path"
9
+ import fs from "fs/promises"
10
+ import iconv from "iconv-lite"
11
+ import { Agent } from "../../src/agent/agent"
12
+ import { AppFileSystem } from "@saeeol/core/filesystem"
13
+ import { ApplyPatchTool } from "../../src/tool/apply_patch"
14
+ import { Bus } from "../../src/bus"
15
+ import * as CrossSpawnSpawner from "@saeeol/core/cross-spawn-spawner"
16
+ import { EditTool } from "../../src/tool/edit"
17
+ import { Format } from "../../src/format"
18
+ import { Instance } from "../../src/project/instance"
19
+ import { Instruction } from "../../src/session/instruction"
20
+ import { LSP } from "../../src/lsp/lsp"
21
+ import { MessageID, SessionID } from "../../src/session/schema"
22
+ import { ReadTool } from "../../src/tool/read"
23
+ import * as Tool from "../../src/tool/tool"
24
+ import { Truncate } from "../../src/tool/truncate"
25
+ import { WriteTool } from "../../src/tool/write"
26
+ import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
27
+ import { testEffect } from "../lib/effect"
28
+
29
+ const ctx = {
30
+ sessionID: SessionID.make("ses_test-encoding"),
31
+ messageID: MessageID.make(""),
32
+ callID: "",
33
+ agent: "build",
34
+ abort: AbortSignal.any([]),
35
+ messages: [],
36
+ metadata: () => Effect.void,
37
+ ask: () => Effect.void,
38
+ }
39
+
40
+ afterEach(async () => {
41
+ await disposeAllInstances()
42
+ })
43
+
44
+ const it = testEffect(
45
+ Layer.mergeAll(
46
+ Agent.defaultLayer,
47
+ AppFileSystem.defaultLayer,
48
+ CrossSpawnSpawner.defaultLayer,
49
+ Instruction.defaultLayer,
50
+ LSP.defaultLayer,
51
+ Bus.layer,
52
+ Format.defaultLayer,
53
+ Truncate.defaultLayer,
54
+ ),
55
+ )
56
+
57
+ const runRead = (args: Tool.InferParameters<typeof ReadTool>) =>
58
+ Effect.gen(function* () {
59
+ const info = yield* ReadTool
60
+ const tool = yield* info.init()
61
+ return yield* tool.execute(args, ctx)
62
+ })
63
+
64
+ const runWrite = (args: Tool.InferParameters<typeof WriteTool>) =>
65
+ Effect.gen(function* () {
66
+ const info = yield* WriteTool
67
+ const tool = yield* info.init()
68
+ return yield* tool.execute(args, ctx)
69
+ })
70
+
71
+ const runEdit = (args: Tool.InferParameters<typeof EditTool>) =>
72
+ Effect.gen(function* () {
73
+ const info = yield* EditTool
74
+ const tool = yield* info.init()
75
+ return yield* tool.execute(args, ctx)
76
+ })
77
+
78
+ const runPatch = (args: Tool.InferParameters<typeof ApplyPatchTool>) =>
79
+ Effect.gen(function* () {
80
+ const info = yield* ApplyPatchTool
81
+ const tool = yield* info.init()
82
+ return yield* tool.execute(args, ctx)
83
+ })
84
+
85
+ // FileTime was removed upstream; edit/write no longer require a prior read.
86
+ const markRead = (_filepath: string) => Effect.void
87
+
88
+ // iconv-lite's UTF codecs don't emit BOMs, but this codebase supports
89
+ // "UTF-X with BOM" as a distinct variant. Prepend one here for fixture files
90
+ // that are meant to have one.
91
+ const UTF8_BOM = "utf-8-bom"
92
+ const encodeBytes = (text: string, encoding: string): Buffer => {
93
+ if (encoding === UTF8_BOM) return Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), iconv.encode(text, "utf-8")])
94
+ const lower = encoding.toLowerCase()
95
+ if (lower === "utf-16le") return Buffer.concat([Buffer.from([0xff, 0xfe]), iconv.encode(text, encoding)])
96
+ if (lower === "utf-16be") return Buffer.concat([Buffer.from([0xfe, 0xff]), iconv.encode(text, encoding)])
97
+ if (lower === "utf-32le") return Buffer.concat([Buffer.from([0xff, 0xfe, 0x00, 0x00]), iconv.encode(text, encoding)])
98
+ if (lower === "utf-32be") return Buffer.concat([Buffer.from([0x00, 0x00, 0xfe, 0xff]), iconv.encode(text, encoding)])
99
+ return iconv.encode(text, encoding)
100
+ }
101
+
102
+ // Create a file with the given encoding by writing raw bytes.
103
+ const putEncoded = (filepath: string, text: string, encoding: string) =>
104
+ Effect.promise(async () => {
105
+ await fs.mkdir(path.dirname(filepath), { recursive: true })
106
+ await fs.writeFile(filepath, encodeBytes(text, encoding))
107
+ })
108
+
109
+ const loadDecoded = (filepath: string, encoding: string) =>
110
+ Effect.promise(async () => {
111
+ const bytes = await fs.readFile(filepath)
112
+ if (encoding === UTF8_BOM) {
113
+ const stripped = bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf
114
+ return iconv.decode(stripped ? bytes.subarray(3) : bytes, "utf-8")
115
+ }
116
+ return iconv.decode(bytes, encoding)
117
+ })
118
+
119
+ const loadBytes = (filepath: string) => Effect.promise(() => fs.readFile(filepath))
120
+
121
+ // Sample phrases chosen to exercise each encoding's characteristic byte patterns.
122
+ const samples = {
123
+ utf8: "Hello, world! — £100",
124
+ shiftJis: "こんにちは、世界!日本語のテストです。",
125
+ eucJp: "日本語のEUC-JPテスト文字列です。",
126
+ gb2312: "你好,世界!这是简体中文测试。",
127
+ big5: "你好,世界!這是繁體中文測試。",
128
+ eucKr: "안녕하세요, 세계! 한국어 테스트입니다.",
129
+ windows1251: "Привет, мир! Это тест кириллицы.",
130
+ koi8r: "Привет, мир! КОИ-8 Р тест.",
131
+ }
132
+
133
+ describe("tool encoding preservation", () => {
134
+ describe("ReadTool decodes files with non-UTF-8 encodings", () => {
135
+ const cases: Array<[string, string, string]> = [
136
+ ["UTF-8", "utf-8", samples.utf8],
137
+ ["UTF-8 with BOM", UTF8_BOM, samples.utf8],
138
+ ["UTF-16 LE with BOM", "utf-16le", samples.utf8],
139
+ ["UTF-16 BE with BOM", "utf-16be", samples.utf8],
140
+ ["UTF-32 LE with BOM", "utf-32le", samples.utf8],
141
+ ["UTF-32 BE with BOM", "utf-32be", samples.utf8],
142
+ ["Shift_JIS", "Shift_JIS", samples.shiftJis],
143
+ ["EUC-JP", "euc-jp", samples.eucJp],
144
+ ["GB2312", "gb2312", samples.gb2312],
145
+ ["Big5", "big5", samples.big5],
146
+ ["EUC-KR", "euc-kr", samples.eucKr],
147
+ ["Windows-1251", "windows-1251", samples.windows1251],
148
+ ["KOI8-R", "koi8-r", samples.koi8r],
149
+ ]
150
+
151
+ for (const [label, encoding, text] of cases) {
152
+ it.live(`decodes ${label} content for the model`, () =>
153
+ provideEncoded(encoding, text, (filepath) =>
154
+ Effect.gen(function* () {
155
+ const result = yield* runRead({ filePath: filepath })
156
+ expect(result.output).toContain(text)
157
+ }),
158
+ ),
159
+ )
160
+ }
161
+ })
162
+
163
+ describe("ReadTool does not flag non-Latin text files as binary", () => {
164
+ it.live("accepts Shift_JIS", () =>
165
+ provideEncoded("Shift_JIS", samples.shiftJis, (filepath) =>
166
+ Effect.gen(function* () {
167
+ const result = yield* runRead({ filePath: filepath })
168
+ expect(result.output).toContain(samples.shiftJis)
169
+ }),
170
+ ),
171
+ )
172
+
173
+ it.live("accepts UTF-16 LE with BOM (contains NUL bytes)", () =>
174
+ provideEncoded("utf-16le", samples.utf8, (filepath) =>
175
+ Effect.gen(function* () {
176
+ const result = yield* runRead({ filePath: filepath })
177
+ expect(result.output).toContain(samples.utf8)
178
+ }),
179
+ ),
180
+ )
181
+
182
+ it.live("accepts UTF-32 LE with BOM (3 of every 4 bytes are NUL)", () =>
183
+ provideEncoded("utf-32le", samples.utf8, (filepath) =>
184
+ Effect.gen(function* () {
185
+ const result = yield* runRead({ filePath: filepath })
186
+ expect(result.output).toContain(samples.utf8)
187
+ }),
188
+ ),
189
+ )
190
+ })
191
+
192
+ describe("WriteTool preserves existing file encoding when overwriting", () => {
193
+ const cases: Array<[string, string, string]> = [
194
+ ["UTF-8 with BOM", UTF8_BOM, samples.utf8],
195
+ ["Shift_JIS", "Shift_JIS", samples.shiftJis],
196
+ ["GB2312", "gb2312", samples.gb2312],
197
+ ["Windows-1251", "windows-1251", samples.windows1251],
198
+ ["UTF-16 LE", "utf-16le", samples.utf8],
199
+ ["UTF-32 LE", "utf-32le", samples.utf8],
200
+ ["UTF-32 BE", "utf-32be", samples.utf8],
201
+ ]
202
+
203
+ for (const [label, encoding, original] of cases) {
204
+ it.live(`preserves ${label} encoding on overwrite`, () =>
205
+ provideTmpdirInstance((dir) =>
206
+ Effect.gen(function* () {
207
+ const filepath = path.join(dir, "file.txt")
208
+ yield* putEncoded(filepath, original, encoding)
209
+ yield* markRead(filepath)
210
+
211
+ const replacement = original + " updated"
212
+ yield* runWrite({ filePath: filepath, content: replacement })
213
+
214
+ const decoded = yield* loadDecoded(filepath, encoding)
215
+ expect(decoded).toBe(replacement)
216
+
217
+ // Bytes should still match the original encoding (and differ from UTF-8).
218
+ const bytes = yield* loadBytes(filepath)
219
+ expect(bytes.equals(encodeBytes(replacement, encoding))).toBe(true)
220
+ }),
221
+ ),
222
+ )
223
+ }
224
+
225
+ it.live("defaults new files to UTF-8", () =>
226
+ provideTmpdirInstance((dir) =>
227
+ Effect.gen(function* () {
228
+ const filepath = path.join(dir, "new.txt")
229
+ yield* runWrite({ filePath: filepath, content: samples.utf8 })
230
+
231
+ const bytes = yield* loadBytes(filepath)
232
+ expect(bytes.equals(Buffer.from(samples.utf8, "utf-8"))).toBe(true)
233
+ }),
234
+ ),
235
+ )
236
+
237
+ // Guard against double-BOM regressions: if the model ever hands back content
238
+ // that already starts with U+FEFF (e.g. by round-tripping literal bytes),
239
+ // writing it to a BOM-encoded file must still produce exactly one BOM.
240
+ const bomCases: Array<[string, string, Buffer]> = [
241
+ ["UTF-8 with BOM", UTF8_BOM, Buffer.from([0xef, 0xbb, 0xbf])],
242
+ ["UTF-16 LE", "utf-16le", Buffer.from([0xff, 0xfe])],
243
+ ["UTF-16 BE", "utf-16be", Buffer.from([0xfe, 0xff])],
244
+ ["UTF-32 LE", "utf-32le", Buffer.from([0xff, 0xfe, 0x00, 0x00])],
245
+ ["UTF-32 BE", "utf-32be", Buffer.from([0x00, 0x00, 0xfe, 0xff])],
246
+ ]
247
+ for (const [label, encoding, bom] of bomCases) {
248
+ it.live(`does not emit a double BOM for ${label} when content starts with U+FEFF`, () =>
249
+ provideTmpdirInstance((dir) =>
250
+ Effect.gen(function* () {
251
+ const filepath = path.join(dir, "file.txt")
252
+ yield* putEncoded(filepath, "hello", encoding)
253
+ yield* markRead(filepath)
254
+
255
+ yield* runWrite({ filePath: filepath, content: "\uFEFFgoodbye" })
256
+
257
+ const bytes = yield* loadBytes(filepath)
258
+ // Exactly one BOM prefix, immediately followed by encoded payload.
259
+ expect(bytes.subarray(0, bom.length).equals(bom)).toBe(true)
260
+ expect(bytes.subarray(bom.length, bom.length * 2).equals(bom)).toBe(false)
261
+ const decoded = yield* loadDecoded(filepath, encoding)
262
+ expect(decoded).toBe("goodbye")
263
+ }),
264
+ ),
265
+ )
266
+ }
267
+ })
268
+
269
+ describe("EditTool preserves existing file encoding across edits", () => {
270
+ const cases: Array<[string, string, string, string, string]> = [
271
+ ["UTF-8 with BOM", UTF8_BOM, samples.utf8 + "\n second line", "world", "earth"],
272
+ ["Shift_JIS", "Shift_JIS", samples.shiftJis, "日本語", "ニホンゴ"],
273
+ ["GB2312", "gb2312", samples.gb2312, "简体中文", "中文简体"],
274
+ ["Windows-1251", "windows-1251", samples.windows1251, "мир", "планета"],
275
+ ["UTF-16 LE", "utf-16le", samples.utf8 + "\n second line", "world", "earth"],
276
+ ["UTF-32 LE", "utf-32le", samples.utf8 + "\n second line", "world", "earth"],
277
+ ]
278
+
279
+ for (const [label, encoding, original, oldString, newString] of cases) {
280
+ it.live(`preserves ${label} through edit`, () =>
281
+ provideTmpdirInstance((dir) =>
282
+ Effect.gen(function* () {
283
+ const filepath = path.join(dir, "doc.txt")
284
+ yield* putEncoded(filepath, original, encoding)
285
+ yield* markRead(filepath)
286
+
287
+ yield* runEdit({ filePath: filepath, oldString, newString })
288
+
289
+ const decoded = yield* loadDecoded(filepath, encoding)
290
+ const expected = original.replace(oldString, newString)
291
+ expect(decoded).toBe(expected)
292
+
293
+ const bytes = yield* loadBytes(filepath)
294
+ expect(bytes.equals(encodeBytes(expected, encoding))).toBe(true)
295
+ }),
296
+ ),
297
+ )
298
+ }
299
+ })
300
+
301
+ describe("ApplyPatchTool preserves encoding", () => {
302
+ it.live("preserves Shift_JIS through an update hunk", () =>
303
+ provideTmpdirInstance((dir) =>
304
+ Effect.gen(function* () {
305
+ const filepath = path.join(dir, "doc.txt")
306
+ const replacement = "日本語"
307
+ const original = "line1\n" + samples.shiftJis + "\nline3\n"
308
+ const expected = original.replace(samples.shiftJis, replacement)
309
+ yield* putEncoded(filepath, original, "Shift_JIS")
310
+
311
+ const patch = [
312
+ "*** Begin Patch",
313
+ "*** Update File: doc.txt",
314
+ "@@",
315
+ " line1",
316
+ "-" + samples.shiftJis,
317
+ "+" + replacement,
318
+ " line3",
319
+ "*** End Patch",
320
+ ].join("\n")
321
+
322
+ yield* runPatch({ patchText: patch })
323
+
324
+ const decoded = yield* loadDecoded(filepath, "Shift_JIS")
325
+ expect(decoded).toBe(expected)
326
+
327
+ // Bytes must still be Shift_JIS, not silently promoted to UTF-8.
328
+ const bytes = yield* loadBytes(filepath)
329
+ expect(bytes.equals(encodeBytes(expected, "Shift_JIS"))).toBe(true)
330
+ }),
331
+ ),
332
+ )
333
+
334
+ // Regression guard: the diff and additions/deletions counts surfaced to the
335
+ // user (and to the permission prompt) are derived from the pre-patch read
336
+ // of the file. A previous version reused a hard-coded UTF-8 decoder for
337
+ // that read, producing mojibake for any non-UTF-8 file. The bytes ended up
338
+ // correct because the patch helper does its own encoding-aware read, so
339
+ // tests that only checked final file bytes (above) missed the bug.
340
+ it.live("returns a non-mojibake diff for a Shift_JIS update", () =>
341
+ provideTmpdirInstance((dir) =>
342
+ Effect.gen(function* () {
343
+ const filepath = path.join(dir, "doc.txt")
344
+ const replacement = "日本語"
345
+ const original = "line1\n" + samples.shiftJis + "\nline3\n"
346
+ yield* putEncoded(filepath, original, "Shift_JIS")
347
+
348
+ const patch = [
349
+ "*** Begin Patch",
350
+ "*** Update File: doc.txt",
351
+ "@@",
352
+ " line1",
353
+ "-" + samples.shiftJis,
354
+ "+" + replacement,
355
+ " line3",
356
+ "*** End Patch",
357
+ ].join("\n")
358
+
359
+ const result = (yield* runPatch({ patchText: patch })) as {
360
+ metadata: {
361
+ diff: string
362
+ files: Array<{ additions: number; deletions: number }>
363
+ }
364
+ }
365
+
366
+ // The diff must contain the real decoded old/new lines, not a UTF-8
367
+ // misread of the Shift_JIS bytes (which would surface as U+FFFD).
368
+ expect(result.metadata.diff).toContain(samples.shiftJis)
369
+ expect(result.metadata.diff).toContain(replacement)
370
+ expect(result.metadata.diff).not.toContain("\uFFFD")
371
+
372
+ // Per-file stats are derived from the same diff, so a mojibake read
373
+ // would inflate both additions and deletions.
374
+ expect(result.metadata.files).toHaveLength(1)
375
+ expect(result.metadata.files[0].additions).toBe(1)
376
+ expect(result.metadata.files[0].deletions).toBe(1)
377
+ }),
378
+ ),
379
+ )
380
+
381
+ it.live("new files added via apply_patch are UTF-8", () =>
382
+ provideTmpdirInstance((dir) =>
383
+ Effect.gen(function* () {
384
+ const patch = ["*** Begin Patch", "*** Add File: new.txt", "+hello world", "*** End Patch"].join("\n")
385
+ yield* runPatch({ patchText: patch })
386
+ const bytes = yield* loadBytes(path.join(dir, "new.txt"))
387
+ expect(bytes.equals(Buffer.from("hello world\n", "utf-8"))).toBe(true)
388
+ }),
389
+ ),
390
+ )
391
+
392
+ // Deletes exercise a code path in patch/index.ts that doesn't write bytes
393
+ // back — verify it still works when the target file is non-UTF-8, because
394
+ // the deletion code has to decode the old contents to confirm match.
395
+ it.live("deletes a Windows-1251 file without UTF-8 corruption errors", () =>
396
+ provideTmpdirInstance((dir) =>
397
+ Effect.gen(function* () {
398
+ const filepath = path.join(dir, "legacy.txt")
399
+ yield* putEncoded(filepath, samples.windows1251, "windows-1251")
400
+ const patch = ["*** Begin Patch", "*** Delete File: legacy.txt", "*** End Patch"].join("\n")
401
+ yield* runPatch({ patchText: patch })
402
+ const exists = yield* Effect.promise(() =>
403
+ fs
404
+ .access(filepath)
405
+ .then(() => true)
406
+ .catch(() => false),
407
+ )
408
+ expect(exists).toBe(false)
409
+ }),
410
+ ),
411
+ )
412
+ })
413
+
414
+ // EditTool's replaceAll path rewrites the entire buffer and re-encodes it
415
+ // in one shot — regression guard that re-encoding a multi-occurrence edit in
416
+ // a legacy encoding yields byte-exact output.
417
+ describe("EditTool replaceAll preserves non-UTF-8 encoding", () => {
418
+ it.live("replaces every occurrence in Shift_JIS", () =>
419
+ provideTmpdirInstance((dir) =>
420
+ Effect.gen(function* () {
421
+ const filepath = path.join(dir, "doc.txt")
422
+ // Pad with additional Shift_JIS text so chardet has enough bytes
423
+ // to confidently identify the encoding.
424
+ const pad = samples.shiftJis + "\n"
425
+ const original = pad + "日本語\n日本語\n日本語\n" + pad
426
+ yield* putEncoded(filepath, original, "Shift_JIS")
427
+ yield* markRead(filepath)
428
+
429
+ yield* runEdit({ filePath: filepath, oldString: "日本語", newString: "ニホンゴ", replaceAll: true })
430
+
431
+ const expected =
432
+ pad.replaceAll("日本語", "ニホンゴ") +
433
+ "ニホンゴ\nニホンゴ\nニホンゴ\n" +
434
+ pad.replaceAll("日本語", "ニホンゴ")
435
+ const decoded = yield* loadDecoded(filepath, "Shift_JIS")
436
+ expect(decoded).toBe(expected)
437
+ const bytes = yield* loadBytes(filepath)
438
+ expect(bytes.equals(encodeBytes(expected, "Shift_JIS"))).toBe(true)
439
+ }),
440
+ ),
441
+ )
442
+ })
443
+ })
444
+
445
+ // Shared helper to set up a temp instance with an encoded file at `file.txt`.
446
+ function provideEncoded<A, E, R>(encoding: string, text: string, body: (filepath: string) => Effect.Effect<A, E, R>) {
447
+ return provideTmpdirInstance((dir) =>
448
+ Effect.gen(function* () {
449
+ const filepath = path.join(dir, "file.txt")
450
+ yield* putEncoded(filepath, text, encoding)
451
+ yield* markRead(filepath)
452
+ return yield* body(filepath)
453
+ }),
454
+ )
455
+ }
@@ -0,0 +1,49 @@
1
+ import { afterEach, describe, expect, mock, spyOn } from "bun:test"
2
+ import { Effect, Layer } from "effect"
3
+ import * as Log from "@saeeol/core/util/log"
4
+ import { Instance } from "../../src/project/instance"
5
+ import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
6
+ import * as CrossSpawnSpawner from "@saeeol/core/cross-spawn-spawner"
7
+ import { testEffect } from "../lib/effect"
8
+
9
+ const err = new Error("indexing import failed")
10
+
11
+ mock.module("@/saeeol/indexing", () => {
12
+ throw err
13
+ })
14
+
15
+ const { ToolRegistry } = await import("../../src/tool/registry")
16
+
17
+ const node = CrossSpawnSpawner.defaultLayer
18
+ const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
19
+
20
+ afterEach(async () => {
21
+ await disposeAllInstances()
22
+ })
23
+
24
+ describe("saeeol tool registry indexing import failure", () => {
25
+ it.live("keeps non-indexing tools when the indexing module cannot load", () =>
26
+ provideTmpdirInstance(
27
+ () =>
28
+ Effect.gen(function* () {
29
+ const logger = Log.create({ service: "saeeol-tool-registry" })
30
+ const warn = spyOn(logger, "warn").mockImplementation(() => {})
31
+
32
+ try {
33
+ const registry = yield* ToolRegistry.Service
34
+ const ids = yield* registry.ids()
35
+
36
+ expect(ids).not.toContain("semantic_search")
37
+ expect(ids).toContain("question")
38
+ expect(ids).toContain("read")
39
+ expect(ids).toContain("suggest")
40
+ expect(warn.mock.calls[0]?.[0]).toBe("semantic search unavailable")
41
+ expect(warn.mock.calls[0]?.[1]?.err).toBeDefined()
42
+ } finally {
43
+ warn.mockRestore()
44
+ }
45
+ }),
46
+ { git: true },
47
+ ),
48
+ )
49
+ })