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.
- package/.turbo/turbo-typecheck.log +1 -0
- package/AGENTS.md +72 -0
- package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
- package/Dockerfile +18 -0
- package/assets/saeeol.ico +0 -0
- package/bin/saeeol.cjs +0 -0
- package/database.db +0 -0
- package/drizzle.config.ts +10 -0
- package/git +0 -0
- package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
- package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
- package/migration/20260211171708_add_project_commands/migration.sql +1 -0
- package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
- package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
- package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
- package/migration/20260225215848_workspace/migration.sql +7 -0
- package/migration/20260225215848_workspace/snapshot.json +959 -0
- package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
- package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
- package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
- package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
- package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
- package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
- package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
- package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
- package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
- package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
- package/migration/20260323234822_events/migration.sql +13 -0
- package/migration/20260323234822_events/snapshot.json +1271 -0
- package/migration/20260410174513_workspace-name/migration.sql +16 -0
- package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
- package/migration/20260413175956_chief_energizer/migration.sql +13 -0
- package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
- package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
- package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
- package/migration/20260428004200_add_session_path/migration.sql +1 -0
- package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
- package/npm/bin/saeeol +42 -0
- package/npm/package.json +39 -0
- package/npm/postinstall.js +162 -0
- package/package.json +201 -207
- package/parsers-config.ts +289 -0
- package/script/build.ts +393 -0
- package/script/check-migrations.ts +16 -0
- package/script/fix-node-pty.ts +34 -0
- package/script/generate.ts +23 -0
- package/script/postinstall.mjs +189 -0
- package/script/publish.ts +200 -0
- package/script/run-workspace-server +106 -0
- package/script/schema.ts +63 -0
- package/script/test-runner.ts +420 -0
- package/script/time.ts +6 -0
- package/script/trace-imports.ts +153 -0
- package/script/upgrade-opentui.ts +64 -0
- package/scripts/diff-sdk-types.sh +52 -0
- package/specs/effect/facades.md +221 -0
- package/specs/effect/http-api.md +401 -0
- package/specs/effect/instance-context.md +309 -0
- package/specs/effect/loose-ends.md +34 -0
- package/specs/effect/migration.md +299 -0
- package/specs/effect/routes.md +64 -0
- package/specs/effect/schema.md +399 -0
- package/specs/effect/server-package.md +668 -0
- package/specs/effect/tools.md +90 -0
- package/specs/tui-plugins.md +433 -0
- package/specs/v2/api.ts +67 -0
- package/specs/v2/keymappings.md +10 -0
- package/specs/v2/message-shape.md +136 -0
- package/src/acp/agent-message.ts +1 -1
- package/src/acp/agent-utils.ts +1 -1
- package/src/boxes/ansi.ts +17 -0
- package/src/boxes/atomic-write.ts +35 -0
- package/src/boxes/b64.ts +58 -0
- package/src/boxes/bash-security.ts +129 -0
- package/src/boxes/bom.ts +18 -0
- package/src/boxes/cancel.ts +16 -0
- package/src/boxes/chop.ts +12 -0
- package/src/boxes/clamp.ts +3 -0
- package/src/boxes/compact.ts +9 -0
- package/src/boxes/cost-tracker.ts +116 -0
- package/src/boxes/dataurl.ts +29 -0
- package/src/boxes/delay.ts +27 -0
- package/src/boxes/diff-apply.ts +53 -0
- package/src/boxes/disposable.ts +13 -0
- package/src/boxes/err.ts +34 -0
- package/src/boxes/human.ts +47 -0
- package/src/boxes/iife.ts +9 -0
- package/src/boxes/latch.ts +8 -0
- package/src/boxes/memory.ts +198 -0
- package/src/boxes/net.ts +16 -0
- package/src/boxes/plural.ts +4 -0
- package/src/boxes/puny.ts +21 -0
- package/src/boxes/retry.ts +49 -0
- package/src/boxes/rwlock.ts +41 -0
- package/src/boxes/schedule.ts +71 -0
- package/src/boxes/scope.ts +21 -0
- package/src/boxes/tokens.ts +9 -0
- package/src/boxes/ttl-cache.ts +63 -0
- package/src/boxes/typed-event.ts +51 -0
- package/src/boxes/uid.ts +50 -0
- package/src/boxes/wave6.test.ts +296 -0
- package/src/boxes/wildcard.ts +58 -0
- package/src/bus/global.ts +1 -1
- package/src/cli/cmd/github-run-api.ts +2 -2
- package/src/cli/cmd/run-events.ts +2 -2
- package/src/cli/cmd/tui/component/logo.tsx +1 -1
- package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
- package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
- package/src/cli/cmd/tui/context/app/editor.ts +1 -1
- package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
- package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
- package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
- package/src/overlay/cost-tracker/format.ts +1 -1
- package/src/overlay/cost-tracker/index.ts +4 -4
- package/src/overlay/cost-tracker/state.ts +2 -2
- package/src/overlay/cost-tracker/types.ts +2 -2
- package/src/overlay/memory/age.ts +1 -1
- package/src/overlay/memory/index.ts +4 -4
- package/src/overlay/memory/paths.ts +2 -2
- package/src/overlay/memory/scan.ts +1 -1
- package/src/overlay/memory/types.ts +2 -2
- package/src/overlay/tool/bash-security.ts +3 -3
- package/src/overlay/util/url.ts +1 -1
- package/src/plugin/codex-auth.ts +1 -1
- package/src/provider/model-cache.ts +2 -2
- package/src/provider/provider-resolve.ts +3 -3
- package/src/provider/transform-message.ts +1 -1
- package/src/server/routes/game.ts +284 -0
- package/src/server/server.ts +2 -0
- package/src/session/core/compaction/compaction-helpers.ts +1 -1
- package/src/session/core/compaction/compaction.ts +1 -1
- package/src/session/core/session.ts +2 -0
- package/src/sessions/ingest-queue.ts +2 -2
- package/src/sessions/remote-ws.ts +1 -1
- package/src/tool/workflow/question.ts +1 -1
- package/src/util/abort.ts +1 -1
- package/src/util/bom.ts +2 -2
- package/src/util/color.ts +1 -1
- package/src/util/data-url.ts +1 -1
- package/src/util/defer.ts +1 -1
- package/src/util/error.ts +2 -2
- package/src/util/filesystem.ts +2 -2
- package/src/util/format.ts +1 -1
- package/src/util/iife.ts +1 -1
- package/src/util/local-context.ts +1 -1
- package/src/util/locale.ts +2 -2
- package/src/util/lock.ts +1 -1
- package/src/util/network.ts +1 -1
- package/src/util/signal.ts +1 -1
- package/src/util/token.ts +1 -1
- package/src/util/wildcard.ts +1 -1
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +133 -0
- package/test/account/repo.test.ts +352 -0
- package/test/account/service.test.ts +456 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +725 -0
- package/test/agent/agent.test.ts +890 -0
- package/test/auth/auth.test.ts +86 -0
- package/test/bun/registry.test.ts +75 -0
- package/test/bus/bus-effect.test.ts +161 -0
- package/test/bus/bus-integration.test.ts +87 -0
- package/test/bus/bus.test.ts +219 -0
- package/test/cli/account.test.ts +26 -0
- package/test/cli/auto-mode.test.ts +75 -0
- package/test/cli/bin-saeeol.test.ts +8 -0
- package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
- package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
- package/test/cli/cmd/tui/sync.test.tsx +159 -0
- package/test/cli/error.test.ts +18 -0
- package/test/cli/github-action.test.ts +198 -0
- package/test/cli/github-remote.test.ts +85 -0
- package/test/cli/import.test.ts +97 -0
- package/test/cli/install-artifact.test.ts +72 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/pr.test.ts +59 -0
- package/test/cli/tui/editor-context-zed.test.ts +356 -0
- package/test/cli/tui/editor-context.test.tsx +228 -0
- package/test/cli/tui/keybind-plugin.test.ts +90 -0
- package/test/cli/tui/markdown.test.ts +161 -0
- package/test/cli/tui/plugin-add.test.ts +111 -0
- package/test/cli/tui/plugin-install.test.ts +87 -0
- package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
- package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
- package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
- package/test/cli/tui/plugin-loader.test.ts +816 -0
- package/test/cli/tui/plugin-toggle.test.ts +157 -0
- package/test/cli/tui/revert-diff.test.ts +35 -0
- package/test/cli/tui/slot-replace.test.tsx +47 -0
- package/test/cli/tui/theme-store.test.ts +54 -0
- package/test/cli/tui/thread.test.ts +28 -0
- package/test/cli/tui/transcript.test.ts +426 -0
- package/test/cli/tui/usage.test.ts +60 -0
- package/test/cli/tui/use-event.test.tsx +175 -0
- package/test/config/agent-color.test.ts +67 -0
- package/test/config/config.test.ts +2544 -0
- package/test/config/fixtures/empty-frontmatter.md +4 -0
- package/test/config/fixtures/frontmatter.md +28 -0
- package/test/config/fixtures/markdown-header.md +11 -0
- package/test/config/fixtures/no-frontmatter.md +1 -0
- package/test/config/fixtures/weird-model-id.md +13 -0
- package/test/config/lsp.test.ts +87 -0
- package/test/config/markdown.test.ts +228 -0
- package/test/config/plugin.test.ts +0 -0
- package/test/config/tui.test.ts +624 -0
- package/test/control-plane/adapters.test.ts +71 -0
- package/test/control-plane/workspace.test.ts +1526 -0
- package/test/effect/app-runtime-logger.test.ts +98 -0
- package/test/effect/config-service.test.ts +65 -0
- package/test/effect/instance-state.test.ts +394 -0
- package/test/effect/run-service.test.ts +89 -0
- package/test/effect/runner.test.ts +523 -0
- package/test/fake/provider.ts +82 -0
- package/test/file/fsmonitor.test.ts +68 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +954 -0
- package/test/file/path-traversal.test.ts +205 -0
- package/test/file/ripgrep.test.ts +226 -0
- package/test/file/watcher.test.ts +249 -0
- package/test/filesystem/filesystem.test.ts +319 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.test.ts +26 -0
- package/test/fixture/fixture.ts +175 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/log-init-worker.ts +62 -0
- package/test/fixture/lsp/fake-lsp-server.js +249 -0
- package/test/fixture/plug-worker.ts +93 -0
- package/test/fixture/plugin-meta-worker.ts +19 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -0
- package/test/fixture/tui-plugin.ts +323 -0
- package/test/fixture/tui-runtime.ts +31 -0
- package/test/format/format.test.ts +272 -0
- package/test/git/git.test.ts +128 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/installation/installation.test.ts +168 -0
- package/test/keybind.test.ts +421 -0
- package/test/lib/effect.ts +53 -0
- package/test/lib/filesystem.ts +10 -0
- package/test/lib/llm-server.ts +778 -0
- package/test/lib/websocket.ts +46 -0
- package/test/lsp/client.test.ts +482 -0
- package/test/lsp/index.test.ts +160 -0
- package/test/lsp/launch.test.ts +22 -0
- package/test/lsp/lifecycle.test.ts +184 -0
- package/test/ltm/ltm.test.ts +230 -0
- package/test/mcp/headers.test.ts +178 -0
- package/test/mcp/lifecycle.test.ts +787 -0
- package/test/mcp/oauth-auto-connect.test.ts +311 -0
- package/test/mcp/oauth-browser.test.ts +276 -0
- package/test/mcp/oauth-callback.test.ts +34 -0
- package/test/memory/abort-leak-webfetch.ts +49 -0
- package/test/memory/abort-leak.test.ts +128 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +1227 -0
- package/test/permission/next.toConfig.test.ts +110 -0
- package/test/permission-task.test.ts +326 -0
- package/test/plugin/auth-override.test.ts +79 -0
- package/test/plugin/cloudflare.test.ts +68 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/plugin/github-copilot-models.test.ts +261 -0
- package/test/plugin/install-concurrency.test.ts +140 -0
- package/test/plugin/install.test.ts +570 -0
- package/test/plugin/loader-shared.test.ts +1169 -0
- package/test/plugin/meta.test.ts +137 -0
- package/test/plugin/plugin-contract.test.ts +291 -0
- package/test/plugin/shared.test.ts +88 -0
- package/test/plugin/trigger.test.ts +102 -0
- package/test/plugin/workspace-adapter.test.ts +109 -0
- package/test/preload.ts +77 -0
- package/test/project/instance.test.ts +276 -0
- package/test/project/migrate-global.test.ts +152 -0
- package/test/project/project.test.ts +600 -0
- package/test/project/vcs.test.ts +286 -0
- package/test/project/worktree-remove.test.ts +126 -0
- package/test/project/worktree.test.ts +223 -0
- package/test/provider/amazon-bedrock.test.ts +462 -0
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
- package/test/provider/gitlab-duo.test.ts +413 -0
- package/test/provider/local.test.ts +208 -0
- package/test/provider/models.test.ts +261 -0
- package/test/provider/provider-category.test.ts +190 -0
- package/test/provider/provider.test.ts +2758 -0
- package/test/provider/transform.test.ts +3681 -0
- package/test/pty/pty-output-isolation.test.ts +147 -0
- package/test/pty/pty-session.test.ts +102 -0
- package/test/pty/pty-shell.test.ts +104 -0
- package/test/question/question.test.ts +490 -0
- package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
- package/test/saeeol/agent-manager-tool.test.ts +71 -0
- package/test/saeeol/agent-permission-overrides.test.ts +75 -0
- package/test/saeeol/agent-skill-permissions.test.ts +37 -0
- package/test/saeeol/ask-agent-permissions.test.ts +303 -0
- package/test/saeeol/bash-hierarchy.test.ts +64 -0
- package/test/saeeol/bash-permission-metadata.test.ts +66 -0
- package/test/saeeol/bash-security-extended.test.ts +243 -0
- package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
- package/test/saeeol/boxes-integration.test.ts +415 -0
- package/test/saeeol/builtin-skills.test.ts +75 -0
- package/test/saeeol/cleanup.ts +28 -0
- package/test/saeeol/cli/dev-setup.test.ts +74 -0
- package/test/saeeol/cli/roll-call.test.ts +161 -0
- package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
- package/test/saeeol/codex-auth-refresh.test.ts +124 -0
- package/test/saeeol/commit-message/generate.test.ts +188 -0
- package/test/saeeol/commit-message/git-context.test.ts +303 -0
- package/test/saeeol/commit-message-windows.test.ts +38 -0
- package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
- package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
- package/test/saeeol/compaction-skip-guard.test.ts +224 -0
- package/test/saeeol/compaction-smart-select.test.ts +100 -0
- package/test/saeeol/config/config.test.ts +166 -0
- package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
- package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
- package/test/saeeol/config-gitignore.test.ts +70 -0
- package/test/saeeol/config-injector.test.ts +305 -0
- package/test/saeeol/config-resilience.test.ts +234 -0
- package/test/saeeol/config-validation.test.ts +183 -0
- package/test/saeeol/cost-propagation.test.ts +94 -0
- package/test/saeeol/cost-tracker-extended.test.ts +141 -0
- package/test/saeeol/cost-tracker.test.ts +64 -0
- package/test/saeeol/custom-provider-delete.test.ts +149 -0
- package/test/saeeol/diff-full.test.ts +226 -0
- package/test/saeeol/edit-permission-filediff.test.ts +223 -0
- package/test/saeeol/encoding.test.ts +364 -0
- package/test/saeeol/enhance-prompt.test.ts +61 -0
- package/test/saeeol/ensure-plan-dir.test.ts +32 -0
- package/test/saeeol/errors.test.ts +144 -0
- package/test/saeeol/external-directory-boundary.test.ts +96 -0
- package/test/saeeol/gateway-headers.test.ts +88 -0
- package/test/saeeol/help.test.ts +191 -0
- package/test/saeeol/ignore-migrator.test.ts +308 -0
- package/test/saeeol/indexing-auth.test.ts +45 -0
- package/test/saeeol/indexing-feature.test.ts +44 -0
- package/test/saeeol/indexing-label.test.ts +70 -0
- package/test/saeeol/indexing-startup.test.ts +381 -0
- package/test/saeeol/indexing-worktree.test.ts +73 -0
- package/test/saeeol/instruction.test.ts +136 -0
- package/test/saeeol/lancedb-runtime.test.ts +116 -0
- package/test/saeeol/loader-auth.test.ts +168 -0
- package/test/saeeol/local-model.test.ts +621 -0
- package/test/saeeol/logo.test.ts +31 -0
- package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
- package/test/saeeol/mcp-branding.test.ts +33 -0
- package/test/saeeol/mcp-docker-rm.test.ts +32 -0
- package/test/saeeol/mcp-migrator.test.ts +736 -0
- package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
- package/test/saeeol/memory-io.test.ts +198 -0
- package/test/saeeol/memory-paths.test.ts +87 -0
- package/test/saeeol/memory-security.test.ts +166 -0
- package/test/saeeol/model-cache-org.test.ts +164 -0
- package/test/saeeol/model-info-panel-utils.test.ts +52 -0
- package/test/saeeol/model-info-panel.types.test.ts +7 -0
- package/test/saeeol/models-401-fallback.test.ts +52 -0
- package/test/saeeol/modes-migrator.test.ts +320 -0
- package/test/saeeol/nvidia-headers.test.ts +74 -0
- package/test/saeeol/patch-jsonc.test.ts +73 -0
- package/test/saeeol/patch.test.ts +172 -0
- package/test/saeeol/paths.test.ts +265 -0
- package/test/saeeol/permission/config-paths.test.ts +174 -0
- package/test/saeeol/permission/env-read.test.ts +149 -0
- package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
- package/test/saeeol/permission/next.always-rules.test.ts +882 -0
- package/test/saeeol/permission/next.reply-http.test.ts +205 -0
- package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
- package/test/saeeol/plan-exit-detection.test.ts +494 -0
- package/test/saeeol/plan-followup.test.ts +1376 -0
- package/test/saeeol/project-config-update.test.ts +120 -0
- package/test/saeeol/project-id.test.ts +455 -0
- package/test/saeeol/provider-cost.test.ts +171 -0
- package/test/saeeol/provider-list-failed-state.test.ts +100 -0
- package/test/saeeol/question-dismiss-all.test.ts +174 -0
- package/test/saeeol/read-directory.test.ts +116 -0
- package/test/saeeol/rules-migrator.test.ts +257 -0
- package/test/saeeol/run-auto.test.ts +176 -0
- package/test/saeeol/run-network.test.ts +224 -0
- package/test/saeeol/semantic-search.test.ts +186 -0
- package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
- package/test/saeeol/session/instruction-substitution.test.ts +72 -0
- package/test/saeeol/session/platform-attribution.test.ts +118 -0
- package/test/saeeol/session/session.test.ts +105 -0
- package/test/saeeol/session-compaction-cap.test.ts +399 -0
- package/test/saeeol/session-compaction-chunks.test.ts +501 -0
- package/test/saeeol/session-compaction-safety.test.ts +481 -0
- package/test/saeeol/session-fork-remap.test.ts +251 -0
- package/test/saeeol/session-import-service.test.ts +114 -0
- package/test/saeeol/session-list.test.ts +47 -0
- package/test/saeeol/session-message-metadata.test.ts +128 -0
- package/test/saeeol/session-overflow.test.ts +78 -0
- package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
- package/test/saeeol/session-processor-network-offline.test.ts +204 -0
- package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
- package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
- package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
- package/test/saeeol/session-prompt-queue.test.ts +815 -0
- package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
- package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
- package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
- package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
- package/test/saeeol/sessions/remote-ws.test.ts +367 -0
- package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
- package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
- package/test/saeeol/snapshot-cache.test.ts +84 -0
- package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
- package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
- package/test/saeeol/stats-subagent-cost.test.ts +123 -0
- package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
- package/test/saeeol/suggestion/suggestion.test.ts +145 -0
- package/test/saeeol/suggestion/tool.test.ts +298 -0
- package/test/saeeol/summary-file-diff.test.ts +28 -0
- package/test/saeeol/system-prompt.test.ts +142 -0
- package/test/saeeol/task-nesting.test.ts +193 -0
- package/test/saeeol/telemetry/feedback.test.ts +8 -0
- package/test/saeeol/todo-view.test.ts +57 -0
- package/test/saeeol/tool-encoding.test.ts +455 -0
- package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
- package/test/saeeol/tool-registry-indexing.test.ts +236 -0
- package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
- package/test/saeeol/tool-task-model.test.ts +352 -0
- package/test/saeeol/transform-opus-4.7.test.ts +89 -0
- package/test/saeeol/tui-diff.test.ts +91 -0
- package/test/saeeol/tui-sync.test.ts +80 -0
- package/test/saeeol/util/url.test.ts +141 -0
- package/test/saeeol/workflows-migrator.test.ts +261 -0
- package/test/saeeol/worktree-diff-summary.test.ts +64 -0
- package/test/saeeol/worktree-diff.test.ts +223 -0
- package/test/saeeol/worktree-remove-lock.test.ts +82 -0
- package/test/server/AGENTS.md +15 -0
- package/test/server/contract.test.ts +357 -0
- package/test/server/experimental-session-list.test.ts +157 -0
- package/test/server/global-session-list.test.ts +155 -0
- package/test/server/httpapi-authorization.test.ts +103 -0
- package/test/server/httpapi-bridge.test.ts +440 -0
- package/test/server/httpapi-config.test.ts +67 -0
- package/test/server/httpapi-cors.test.ts +89 -0
- package/test/server/httpapi-event.test.ts +57 -0
- package/test/server/httpapi-experimental.test.ts +219 -0
- package/test/server/httpapi-file.test.ts +79 -0
- package/test/server/httpapi-instance-context.test.ts +237 -0
- package/test/server/httpapi-instance.legacy.test.ts +140 -0
- package/test/server/httpapi-instance.test.ts +83 -0
- package/test/server/httpapi-json-parity.test.ts +263 -0
- package/test/server/httpapi-mcp-oauth.test.ts +76 -0
- package/test/server/httpapi-mcp.test.ts +189 -0
- package/test/server/httpapi-provider.test.ts +153 -0
- package/test/server/httpapi-pty-websocket.test.ts +16 -0
- package/test/server/httpapi-pty.test.ts +175 -0
- package/test/server/httpapi-raw-route-auth.test.ts +89 -0
- package/test/server/httpapi-sdk.test.ts +681 -0
- package/test/server/httpapi-session.test.ts +464 -0
- package/test/server/httpapi-sync.test.ts +130 -0
- package/test/server/httpapi-tui.test.ts +121 -0
- package/test/server/httpapi-workspace-routing.test.ts +471 -0
- package/test/server/httpapi-workspace.test.ts +427 -0
- package/test/server/lib/conformance.ts +88 -0
- package/test/server/lib/stateful.ts +112 -0
- package/test/server/project-init-git.test.ts +113 -0
- package/test/server/proxy-util.test.ts +113 -0
- package/test/server/session-actions.test.ts +49 -0
- package/test/server/session-list.test.ts +238 -0
- package/test/server/session-messages.test.ts +167 -0
- package/test/server/session-select.test.ts +100 -0
- package/test/server/trace-attributes.test.ts +76 -0
- package/test/server/workspace-proxy.test.ts +165 -0
- package/test/server/workspace-routing.test.ts +85 -0
- package/test/session/compaction.test.ts +2420 -0
- package/test/session/instruction.test.ts +247 -0
- package/test/session/llm.test.ts +1273 -0
- package/test/session/message-v2.test.ts +1291 -0
- package/test/session/messages-pagination.test.ts +1173 -0
- package/test/session/network.test.ts +249 -0
- package/test/session/processor-effect.test.ts +847 -0
- package/test/session/prompt.test.ts +2131 -0
- package/test/session/retry.test.ts +340 -0
- package/test/session/revert-compact.test.ts +639 -0
- package/test/session/schema-decoding.test.ts +311 -0
- package/test/session/session-entry-stepper.test.ts +917 -0
- package/test/session/session-schema.test.ts +76 -0
- package/test/session/snapshot-tool-race.test.ts +257 -0
- package/test/session/structured-output-integration.test.ts +265 -0
- package/test/session/structured-output.test.ts +381 -0
- package/test/session/system.test.ts +73 -0
- package/test/share/share-next.test.ts +333 -0
- package/test/shell/shell.test.ts +99 -0
- package/test/skill/discovery.test.ts +116 -0
- package/test/skill/skill.test.ts +393 -0
- package/test/snapshot/snapshot.test.ts +1531 -0
- package/test/storage/db.test.ts +23 -0
- package/test/storage/json-migration.test.ts +832 -0
- package/test/storage/storage.test.ts +293 -0
- package/test/suggestion/suggestion.test.ts +1 -0
- package/test/sync/index.test.ts +256 -0
- package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +614 -0
- package/test/tool/bash.test.ts +1225 -0
- package/test/tool/diagnostics-filter.test.ts +55 -0
- package/test/tool/edit.test.ts +754 -0
- package/test/tool/external-directory.test.ts +169 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +65179 -0
- package/test/tool/glob.test.ts +107 -0
- package/test/tool/grep.test.ts +114 -0
- package/test/tool/lsp.test.ts +187 -0
- package/test/tool/parameters.test.ts +243 -0
- package/test/tool/question.test.ts +129 -0
- package/test/tool/read.test.ts +500 -0
- package/test/tool/recall.test.ts +151 -0
- package/test/tool/registry.test.ts +203 -0
- package/test/tool/skill.test.ts +135 -0
- package/test/tool/suggest.test.ts +1 -0
- package/test/tool/task.test.ts +612 -0
- package/test/tool/tool-define.test.ts +99 -0
- package/test/tool/truncation.test.ts +260 -0
- package/test/tool/webfetch.test.ts +103 -0
- package/test/tool/write.test.ts +291 -0
- package/test/util/data-url.test.ts +14 -0
- package/test/util/effect-zod.test.ts +754 -0
- package/test/util/error.test.ts +38 -0
- package/test/util/filesystem.test.ts +656 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/glob.test.ts +164 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/lock.test.ts +72 -0
- package/test/util/log.test.ts +86 -0
- package/test/util/module.test.ts +59 -0
- package/test/util/process.test.ts +128 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/which.test.ts +100 -0
- package/test/util/wildcard.test.ts +90 -0
- package/test/workspace/workspace-restore.test.ts +296 -0
- package/src/provider/models-snapshot.d.ts +0 -2
- package/src/provider/models-snapshot.js +0 -3
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// Unit tests for the Encoding namespace. These complement tool-encoding.test.ts
|
|
2
|
+
// by exercising detect/decode/encode/read/write/readSync directly, without
|
|
3
|
+
// going through the Effect runtime, agent harness, or tool pipeline. They are
|
|
4
|
+
// cheap, fast, and cover the internal branches (BOM handling, ASCII/UTF-8
|
|
5
|
+
// normalization, chardet fallback, unsupported encoding rejection) that the
|
|
6
|
+
// integration tests cannot hit deterministically.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test"
|
|
9
|
+
import fs from "fs/promises"
|
|
10
|
+
import os from "os"
|
|
11
|
+
import path from "path"
|
|
12
|
+
import iconv from "iconv-lite"
|
|
13
|
+
import * as Encoding from "../../src/overlay/encoding"
|
|
14
|
+
|
|
15
|
+
const BOM = {
|
|
16
|
+
utf8: Buffer.from([0xef, 0xbb, 0xbf]),
|
|
17
|
+
utf16le: Buffer.from([0xff, 0xfe]),
|
|
18
|
+
utf16be: Buffer.from([0xfe, 0xff]),
|
|
19
|
+
utf32le: Buffer.from([0xff, 0xfe, 0x00, 0x00]),
|
|
20
|
+
utf32be: Buffer.from([0x00, 0x00, 0xfe, 0xff]),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function tmp<T>(body: (dir: string) => Promise<T>): Promise<T> {
|
|
24
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "saeeol-encoding-"))
|
|
25
|
+
try {
|
|
26
|
+
return await body(dir)
|
|
27
|
+
} finally {
|
|
28
|
+
await fs.rm(dir, { recursive: true, force: true })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Encoding.detect", () => {
|
|
33
|
+
test("empty buffer falls back to utf-8", () => {
|
|
34
|
+
expect(Encoding.detect(Buffer.alloc(0))).toBe(Encoding.DEFAULT)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("plain ASCII is reported as utf-8", () => {
|
|
38
|
+
// Plain ASCII is valid UTF-8, so the detector short-circuits on the
|
|
39
|
+
// isUtf8 check and never reaches chardet. UTF-8 is an ASCII superset,
|
|
40
|
+
// so this label round-trips identically through iconv-lite.
|
|
41
|
+
expect(Encoding.detect(Buffer.from("plain ascii text\n"))).toBe("utf-8")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("valid UTF-8 without BOM detects as utf-8", () => {
|
|
45
|
+
expect(Encoding.detect(Buffer.from("Hello — 世界", "utf-8"))).toBe("utf-8")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("UTF-8 with BOM is reported as the distinct utf-8-bom variant", () => {
|
|
49
|
+
const bytes = Buffer.concat([BOM.utf8, Buffer.from("hello", "utf-8")])
|
|
50
|
+
expect(Encoding.detect(bytes)).toBe(Encoding.UTF8_BOM)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("BOM-less UTF-8 containing multi-byte chars is not misdetected", () => {
|
|
54
|
+
// Regression guard: bytes that are valid UTF-8 must skip the chardet
|
|
55
|
+
// branch. Encoding detectors have been known to misfire on short CJK samples.
|
|
56
|
+
expect(Encoding.detect(Buffer.from("한글 テスト 中文", "utf-8"))).toBe("utf-8")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("UTF-16 LE with BOM detects as utf-16le", () => {
|
|
60
|
+
const bytes = Buffer.concat([BOM.utf16le, iconv.encode("hello world", "utf-16le")])
|
|
61
|
+
expect(Encoding.detect(bytes)).toBe("utf-16le")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("UTF-16 BE with BOM detects as utf-16be", () => {
|
|
65
|
+
const bytes = Buffer.concat([BOM.utf16be, iconv.encode("hello world", "utf-16be")])
|
|
66
|
+
expect(Encoding.detect(bytes)).toBe("utf-16be")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("UTF-32 LE with BOM detects as utf-32le", () => {
|
|
70
|
+
const bytes = Buffer.concat([BOM.utf32le, iconv.encode("hello world", "utf-32le")])
|
|
71
|
+
expect(Encoding.detect(bytes)).toBe("utf-32le")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("UTF-32 BE with BOM detects as utf-32be", () => {
|
|
75
|
+
const bytes = Buffer.concat([BOM.utf32be, iconv.encode("hello world", "utf-32be")])
|
|
76
|
+
expect(Encoding.detect(bytes)).toBe("utf-32be")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("UTF-32 LE BOM is not misdetected as UTF-16 LE (shared FF FE prefix)", () => {
|
|
80
|
+
// UTF-32 LE BOM is FF FE 00 00; its first two bytes are identical to the
|
|
81
|
+
// UTF-16 LE BOM, so the order of BOM checks matters.
|
|
82
|
+
const bytes = Buffer.concat([BOM.utf32le, iconv.encode("hi", "utf-32le")])
|
|
83
|
+
expect(Encoding.detect(bytes)).toBe("utf-32le")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("Shift_JIS bytes detect as Shift_JIS (case-insensitive, iconv-compatible label)", () => {
|
|
87
|
+
const bytes = iconv.encode("こんにちは、世界!日本語のテストです。", "Shift_JIS")
|
|
88
|
+
const detected = Encoding.detect(bytes)
|
|
89
|
+
expect(detected.toLowerCase()).toBe("shift_jis")
|
|
90
|
+
// The returned label must be accepted by iconv-lite so downstream decode
|
|
91
|
+
// works without a second normalization step.
|
|
92
|
+
expect(iconv.encodingExists(detected)).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("Windows-1251 bytes detect as windows-1251", () => {
|
|
96
|
+
const bytes = iconv.encode("Привет, мир! Это тест кириллицы.", "windows-1251")
|
|
97
|
+
expect(Encoding.detect(bytes)).toBe("windows-1251")
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe("Encoding.decode / Encoding.encode", () => {
|
|
102
|
+
const cases: Array<[string, string, string]> = [
|
|
103
|
+
["utf-8", "utf-8", "Hello — £100"],
|
|
104
|
+
["utf-8-bom synthetic label", Encoding.UTF8_BOM, "hello"],
|
|
105
|
+
["utf-16le", "utf-16le", "Hello 世界"],
|
|
106
|
+
["utf-16be", "utf-16be", "Hello 世界"],
|
|
107
|
+
["utf-32le", "utf-32le", "Hello 世界"],
|
|
108
|
+
["utf-32be", "utf-32be", "Hello 世界"],
|
|
109
|
+
["Shift_JIS", "Shift_JIS", "日本語"],
|
|
110
|
+
["windows-1251", "windows-1251", "Привет"],
|
|
111
|
+
["gb2312", "gb2312", "你好"],
|
|
112
|
+
["big5", "big5", "繁體"],
|
|
113
|
+
["euc-kr", "euc-kr", "한국어"],
|
|
114
|
+
["koi8-r", "koi8-r", "Привет"],
|
|
115
|
+
["iso-8859-1", "iso-8859-1", "Hëllo Wörld"],
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
for (const [label, encoding, text] of cases) {
|
|
119
|
+
test(`round-trips ${label}`, () => {
|
|
120
|
+
const bytes = Encoding.encode(text, encoding)
|
|
121
|
+
expect(Encoding.decode(bytes, encoding)).toBe(text)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
test("utf-8-bom encode emits exactly one BOM even if input starts with U+FEFF", () => {
|
|
126
|
+
// Regression guard: writers may hand us text that was previously decoded
|
|
127
|
+
// and still carries U+FEFF. The encoder must strip it to avoid doubling.
|
|
128
|
+
const bytes = Encoding.encode("\uFEFFhello", Encoding.UTF8_BOM)
|
|
129
|
+
expect(bytes.subarray(0, 3).equals(BOM.utf8)).toBe(true)
|
|
130
|
+
expect(bytes.subarray(3, 6).equals(BOM.utf8)).toBe(false)
|
|
131
|
+
expect(Encoding.decode(bytes, Encoding.UTF8_BOM)).toBe("hello")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("utf-16le encode emits exactly one BOM even if input starts with U+FEFF", () => {
|
|
135
|
+
const bytes = Encoding.encode("\uFEFFhi", "utf-16le")
|
|
136
|
+
expect(bytes.subarray(0, 2).equals(BOM.utf16le)).toBe(true)
|
|
137
|
+
// Next two bytes must be the 'h' code unit (0x68 0x00), not another BOM.
|
|
138
|
+
expect(bytes[2]).toBe(0x68)
|
|
139
|
+
expect(bytes[3]).toBe(0x00)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("utf-16be encode emits exactly one BOM even if input starts with U+FEFF", () => {
|
|
143
|
+
const bytes = Encoding.encode("\uFEFFhi", "utf-16be")
|
|
144
|
+
expect(bytes.subarray(0, 2).equals(BOM.utf16be)).toBe(true)
|
|
145
|
+
expect(bytes[2]).toBe(0x00)
|
|
146
|
+
expect(bytes[3]).toBe(0x68)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test("utf-32le encode emits exactly one BOM even if input starts with U+FEFF", () => {
|
|
150
|
+
const bytes = Encoding.encode("\uFEFFhi", "utf-32le")
|
|
151
|
+
expect(bytes.subarray(0, 4).equals(BOM.utf32le)).toBe(true)
|
|
152
|
+
// Next four bytes must be the 'h' code point (0x68 0x00 0x00 0x00), not another BOM.
|
|
153
|
+
expect(bytes[4]).toBe(0x68)
|
|
154
|
+
expect(bytes[5]).toBe(0x00)
|
|
155
|
+
expect(bytes[6]).toBe(0x00)
|
|
156
|
+
expect(bytes[7]).toBe(0x00)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("utf-32be encode emits exactly one BOM even if input starts with U+FEFF", () => {
|
|
160
|
+
const bytes = Encoding.encode("\uFEFFhi", "utf-32be")
|
|
161
|
+
expect(bytes.subarray(0, 4).equals(BOM.utf32be)).toBe(true)
|
|
162
|
+
expect(bytes[4]).toBe(0x00)
|
|
163
|
+
expect(bytes[5]).toBe(0x00)
|
|
164
|
+
expect(bytes[6]).toBe(0x00)
|
|
165
|
+
expect(bytes[7]).toBe(0x68)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("decode of utf-8-bom produces text without leading U+FEFF", () => {
|
|
169
|
+
// iconv-lite's utf-8 codec is documented to strip BOMs; guard against
|
|
170
|
+
// regressions if the underlying behaviour changes.
|
|
171
|
+
const bytes = Buffer.concat([BOM.utf8, Buffer.from("abc", "utf-8")])
|
|
172
|
+
expect(Encoding.decode(bytes, Encoding.UTF8_BOM)).toBe("abc")
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe("Encoding.hasUtf16Bom", () => {
|
|
177
|
+
test("detects LE BOM", () => {
|
|
178
|
+
expect(Encoding.hasUtf16Bom(BOM.utf16le)).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
test("detects BE BOM", () => {
|
|
181
|
+
expect(Encoding.hasUtf16Bom(BOM.utf16be)).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
test("returns false for UTF-8 BOM", () => {
|
|
184
|
+
expect(Encoding.hasUtf16Bom(BOM.utf8)).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
test("returns false for plain ASCII", () => {
|
|
187
|
+
expect(Encoding.hasUtf16Bom(Buffer.from("ab"))).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
test("respects an explicit limit smaller than the buffer", () => {
|
|
190
|
+
// Passing limit<2 must treat the sample as too short to contain a BOM,
|
|
191
|
+
// even if the underlying buffer starts with one. This matches the binary
|
|
192
|
+
// detection call site which reads a bounded sample.
|
|
193
|
+
expect(Encoding.hasUtf16Bom(BOM.utf16le, 1)).toBe(false)
|
|
194
|
+
expect(Encoding.hasUtf16Bom(BOM.utf16le, 2)).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
test("returns false for a one-byte buffer", () => {
|
|
197
|
+
expect(Encoding.hasUtf16Bom(Buffer.from([0xff]))).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
test("returns false for a UTF-32 LE BOM (distinct encoding)", () => {
|
|
200
|
+
// UTF-32 LE starts with FF FE 00 00; a naive check on the first two bytes
|
|
201
|
+
// would misclassify it as UTF-16 LE. hasUtf16Bom must reject it so the
|
|
202
|
+
// caller can disambiguate.
|
|
203
|
+
expect(Encoding.hasUtf16Bom(BOM.utf32le)).toBe(false)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe("Encoding.hasUtf32Bom", () => {
|
|
208
|
+
test("detects LE BOM (FF FE 00 00)", () => {
|
|
209
|
+
expect(Encoding.hasUtf32Bom(BOM.utf32le)).toBe(true)
|
|
210
|
+
})
|
|
211
|
+
test("detects BE BOM (00 00 FE FF)", () => {
|
|
212
|
+
expect(Encoding.hasUtf32Bom(BOM.utf32be)).toBe(true)
|
|
213
|
+
})
|
|
214
|
+
test("returns false for UTF-16 LE BOM (shorter, distinct encoding)", () => {
|
|
215
|
+
expect(Encoding.hasUtf32Bom(BOM.utf16le)).toBe(false)
|
|
216
|
+
})
|
|
217
|
+
test("returns false for UTF-16 BE BOM", () => {
|
|
218
|
+
expect(Encoding.hasUtf32Bom(BOM.utf16be)).toBe(false)
|
|
219
|
+
})
|
|
220
|
+
test("returns false for UTF-8 BOM", () => {
|
|
221
|
+
expect(Encoding.hasUtf32Bom(BOM.utf8)).toBe(false)
|
|
222
|
+
})
|
|
223
|
+
test("returns false for a three-byte buffer (too short)", () => {
|
|
224
|
+
expect(Encoding.hasUtf32Bom(Buffer.from([0xff, 0xfe, 0x00]))).toBe(false)
|
|
225
|
+
})
|
|
226
|
+
test("respects an explicit limit smaller than the buffer", () => {
|
|
227
|
+
expect(Encoding.hasUtf32Bom(BOM.utf32le, 3)).toBe(false)
|
|
228
|
+
expect(Encoding.hasUtf32Bom(BOM.utf32le, 4)).toBe(true)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe("Encoding.read / Encoding.readSync / Encoding.write", () => {
|
|
233
|
+
// chardet is noticeably more conservative than other detectors (jschardet,
|
|
234
|
+
// ICU) on tiny samples: a 12-byte Shift_JIS phrase collides with the
|
|
235
|
+
// windows-1252 profile and is misclassified. In practice this is fine,
|
|
236
|
+
// because the tool pipeline only runs detection on files the agent is about
|
|
237
|
+
// to read or patch — real source files and documents carry far more than 12
|
|
238
|
+
// bytes of characteristic content, which is plenty for chardet to lock
|
|
239
|
+
// onto the right encoding. The short-sample cliff only matters for
|
|
240
|
+
// synthetic fixtures like this one, so we pad the sample to the same body
|
|
241
|
+
// of Japanese text the rest of the suite already relies on.
|
|
242
|
+
const shiftJisSample = "こんにちは、世界!日本語のテストです。"
|
|
243
|
+
|
|
244
|
+
test("read detects and decodes Shift_JIS asynchronously", async () => {
|
|
245
|
+
await tmp(async (dir) => {
|
|
246
|
+
const filepath = path.join(dir, "sj.txt")
|
|
247
|
+
await fs.writeFile(filepath, iconv.encode(shiftJisSample, "Shift_JIS"))
|
|
248
|
+
const result = await Encoding.read(filepath)
|
|
249
|
+
expect(result.text).toBe(shiftJisSample)
|
|
250
|
+
expect(result.encoding.toLowerCase()).toBe("shift_jis")
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test("readSync mirrors read for the same input", async () => {
|
|
255
|
+
await tmp(async (dir) => {
|
|
256
|
+
const filepath = path.join(dir, "sj.txt")
|
|
257
|
+
await fs.writeFile(filepath, iconv.encode(shiftJisSample, "Shift_JIS"))
|
|
258
|
+
const sync = Encoding.readSync(filepath)
|
|
259
|
+
const async_ = await Encoding.read(filepath)
|
|
260
|
+
expect(sync).toEqual(async_)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test("read preserves UTF-8 BOM as a distinct encoding label", async () => {
|
|
265
|
+
await tmp(async (dir) => {
|
|
266
|
+
const filepath = path.join(dir, "bom.txt")
|
|
267
|
+
await fs.writeFile(filepath, Buffer.concat([BOM.utf8, Buffer.from("hi", "utf-8")]))
|
|
268
|
+
const result = await Encoding.read(filepath)
|
|
269
|
+
expect(result.encoding).toBe(Encoding.UTF8_BOM)
|
|
270
|
+
expect(result.text).toBe("hi")
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test("write creates missing parent directories", async () => {
|
|
275
|
+
await tmp(async (dir) => {
|
|
276
|
+
const filepath = path.join(dir, "nested", "deeply", "file.txt")
|
|
277
|
+
await Encoding.write(filepath, "hello", "utf-8")
|
|
278
|
+
const bytes = await fs.readFile(filepath)
|
|
279
|
+
expect(bytes.equals(Buffer.from("hello", "utf-8"))).toBe(true)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test("write defaults to utf-8 when encoding is omitted", async () => {
|
|
284
|
+
await tmp(async (dir) => {
|
|
285
|
+
const filepath = path.join(dir, "default.txt")
|
|
286
|
+
await Encoding.write(filepath, "héllo")
|
|
287
|
+
const bytes = await fs.readFile(filepath)
|
|
288
|
+
expect(bytes.equals(Buffer.from("héllo", "utf-8"))).toBe(true)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test("write round-trips Shift_JIS bytes exactly", async () => {
|
|
293
|
+
await tmp(async (dir) => {
|
|
294
|
+
const filepath = path.join(dir, "sj.txt")
|
|
295
|
+
const text = "日本語"
|
|
296
|
+
await Encoding.write(filepath, text, "Shift_JIS")
|
|
297
|
+
const bytes = await fs.readFile(filepath)
|
|
298
|
+
expect(bytes.equals(iconv.encode(text, "Shift_JIS"))).toBe(true)
|
|
299
|
+
// Must not be UTF-8 — regression guard against silent promotion.
|
|
300
|
+
expect(bytes.equals(Buffer.from(text, "utf-8"))).toBe(false)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test("write + read round-trips utf-16le with BOM", async () => {
|
|
305
|
+
await tmp(async (dir) => {
|
|
306
|
+
const filepath = path.join(dir, "u16.txt")
|
|
307
|
+
const text = "Hello 世界"
|
|
308
|
+
await Encoding.write(filepath, text, "utf-16le")
|
|
309
|
+
const bytes = await fs.readFile(filepath)
|
|
310
|
+
expect(bytes.subarray(0, 2).equals(BOM.utf16le)).toBe(true)
|
|
311
|
+
const result = await Encoding.read(filepath)
|
|
312
|
+
expect(result.encoding).toBe("utf-16le")
|
|
313
|
+
expect(result.text).toBe(text)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test("write + read round-trips utf-32le with BOM", async () => {
|
|
318
|
+
await tmp(async (dir) => {
|
|
319
|
+
const filepath = path.join(dir, "u32.txt")
|
|
320
|
+
const text = "Hello 世界"
|
|
321
|
+
await Encoding.write(filepath, text, "utf-32le")
|
|
322
|
+
const bytes = await fs.readFile(filepath)
|
|
323
|
+
expect(bytes.subarray(0, 4).equals(BOM.utf32le)).toBe(true)
|
|
324
|
+
const result = await Encoding.read(filepath)
|
|
325
|
+
expect(result.encoding).toBe("utf-32le")
|
|
326
|
+
expect(result.text).toBe(text)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test("write + read round-trips utf-32be with BOM", async () => {
|
|
331
|
+
await tmp(async (dir) => {
|
|
332
|
+
const filepath = path.join(dir, "u32be.txt")
|
|
333
|
+
const text = "Hello 世界"
|
|
334
|
+
await Encoding.write(filepath, text, "utf-32be")
|
|
335
|
+
const bytes = await fs.readFile(filepath)
|
|
336
|
+
expect(bytes.subarray(0, 4).equals(BOM.utf32be)).toBe(true)
|
|
337
|
+
const result = await Encoding.read(filepath)
|
|
338
|
+
expect(result.encoding).toBe("utf-32be")
|
|
339
|
+
expect(result.text).toBe(text)
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe("Encoding.write with existing parent directories", () => {
|
|
345
|
+
test("creates parent and writes file", async () => {
|
|
346
|
+
await tmp(async (dir) => {
|
|
347
|
+
const filepath = path.join(dir, "subdir", "test.txt")
|
|
348
|
+
await Encoding.write(filepath, "hello")
|
|
349
|
+
const text = await fs.readFile(filepath, "utf8")
|
|
350
|
+
expect(text).toBe("hello")
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test("writes into existing directory (Windows EEXIST resiliency)", async () => {
|
|
355
|
+
await tmp(async (dir) => {
|
|
356
|
+
const existing = path.join(dir, "exists")
|
|
357
|
+
await fs.mkdir(existing, { recursive: true })
|
|
358
|
+
const filepath = path.join(existing, "test.txt")
|
|
359
|
+
await Encoding.write(filepath, "hello")
|
|
360
|
+
const text = await fs.readFile(filepath, "utf8")
|
|
361
|
+
expect(text).toBe("hello")
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test"
|
|
2
|
+
import { clean, INSTRUCTION } from "../../src/overlay/enhance-prompt"
|
|
3
|
+
|
|
4
|
+
describe("enhance-prompt", () => {
|
|
5
|
+
describe("instruction", () => {
|
|
6
|
+
it("treats question-shaped drafts as prompts to rewrite", () => {
|
|
7
|
+
expect(INSTRUCTION).toContain("never as a request to answer")
|
|
8
|
+
expect(INSTRUCTION).toContain("rewrite it into a clearer question or request without answering it")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("improves instruction-shaped drafts instead of following them", () => {
|
|
12
|
+
expect(INSTRUCTION).toContain("improve those instructions instead of following them")
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe("clean", () => {
|
|
17
|
+
it("trims whitespace", () => {
|
|
18
|
+
expect(clean(" hello world ")).toBe("hello world")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("strips code block markers", () => {
|
|
22
|
+
expect(clean("```\nhello world\n```")).toBe("hello world")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("strips code block with language tag", () => {
|
|
26
|
+
expect(clean("```text\nhello world\n```")).toBe("hello world")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("strips surrounding double quotes", () => {
|
|
30
|
+
expect(clean('"hello world"')).toBe("hello world")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("strips surrounding single quotes", () => {
|
|
34
|
+
expect(clean("'hello world'")).toBe("hello world")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("strips code blocks and quotes together", () => {
|
|
38
|
+
expect(clean('```\n"hello world"\n```')).toBe("hello world")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("returns plain text unchanged", () => {
|
|
42
|
+
expect(clean("hello world")).toBe("hello world")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("handles empty string", () => {
|
|
46
|
+
expect(clean("")).toBe("")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("handles whitespace-only string", () => {
|
|
50
|
+
expect(clean(" ")).toBe("")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("does not strip internal quotes", () => {
|
|
54
|
+
expect(clean('say "hello" to the world')).toBe('say "hello" to the world')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("does not strip mismatched quotes", () => {
|
|
58
|
+
expect(clean("\"hello world'")).toBe("\"hello world'")
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import fs from "fs/promises"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { SaeeolSessionPrompt } from "../../src/overlay/session/prompt"
|
|
5
|
+
import { tmpdir } from "../fixture/fixture"
|
|
6
|
+
|
|
7
|
+
describe("SaeeolSessionPrompt.ensurePlanDir", () => {
|
|
8
|
+
test("creates a missing plan directory", async () => {
|
|
9
|
+
await using tmp = await tmpdir({})
|
|
10
|
+
const dir = path.join(tmp.path, ".saeeol", "plans")
|
|
11
|
+
await SaeeolSessionPrompt.ensurePlanDir(dir)
|
|
12
|
+
const stat = await fs.stat(dir)
|
|
13
|
+
expect(stat.isDirectory()).toBe(true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("is idempotent when the directory already exists", async () => {
|
|
17
|
+
await using tmp = await tmpdir({})
|
|
18
|
+
const dir = path.join(tmp.path, ".saeeol", "plans")
|
|
19
|
+
await fs.mkdir(dir, { recursive: true })
|
|
20
|
+
await expect(SaeeolSessionPrompt.ensurePlanDir(dir)).resolves.toBeUndefined()
|
|
21
|
+
const stat = await fs.stat(dir)
|
|
22
|
+
expect(stat.isDirectory()).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test("creates intermediate parent directories", async () => {
|
|
26
|
+
await using tmp = await tmpdir({})
|
|
27
|
+
const dir = path.join(tmp.path, "deep", "nested", ".saeeol", "plans")
|
|
28
|
+
await SaeeolSessionPrompt.ensurePlanDir(dir)
|
|
29
|
+
const stat = await fs.stat(dir)
|
|
30
|
+
expect(stat.isDirectory()).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test"
|
|
2
|
+
import { MessageV2 } from "../../src/session/message-v2"
|
|
3
|
+
import { SAEEOL_ERROR_CODES, isSaeeolError, parseSaeeolErrorCode } from "../../src/overlay/errors"
|
|
4
|
+
import { SessionRetry } from "../../src/session/retry"
|
|
5
|
+
import { NamedError } from "@saeeol/core/util/error"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper to create a mock APIError object (as returned by .toObject())
|
|
9
|
+
*/
|
|
10
|
+
function makeAPIError(opts: {
|
|
11
|
+
statusCode?: number
|
|
12
|
+
isRetryable?: boolean
|
|
13
|
+
responseBody?: string
|
|
14
|
+
message?: string
|
|
15
|
+
}): ReturnType<NamedError["toObject"]> {
|
|
16
|
+
return new MessageV2.APIError({
|
|
17
|
+
message: opts.message ?? "Error",
|
|
18
|
+
statusCode: opts.statusCode,
|
|
19
|
+
isRetryable: opts.isRetryable ?? false,
|
|
20
|
+
responseBody: opts.responseBody,
|
|
21
|
+
}).toObject()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("parseSaeeolErrorCode", () => {
|
|
25
|
+
it("extracts PAID_MODEL_AUTH_REQUIRED from { error: { code } }", () => {
|
|
26
|
+
const error = makeAPIError({
|
|
27
|
+
statusCode: 401,
|
|
28
|
+
responseBody: JSON.stringify({ error: { code: "PAID_MODEL_AUTH_REQUIRED" } }),
|
|
29
|
+
})
|
|
30
|
+
expect(parseSaeeolErrorCode(error)).toBe("PAID_MODEL_AUTH_REQUIRED")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("extracts PROMOTION_MODEL_LIMIT_REACHED from { code } (top-level)", () => {
|
|
34
|
+
const error = makeAPIError({
|
|
35
|
+
statusCode: 429,
|
|
36
|
+
responseBody: JSON.stringify({ code: "PROMOTION_MODEL_LIMIT_REACHED" }),
|
|
37
|
+
})
|
|
38
|
+
expect(parseSaeeolErrorCode(error)).toBe("PROMOTION_MODEL_LIMIT_REACHED")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("extracts PROMOTION_MODEL_LIMIT_REACHED from { error: { code } }", () => {
|
|
42
|
+
const error = makeAPIError({
|
|
43
|
+
statusCode: 401,
|
|
44
|
+
responseBody: JSON.stringify({
|
|
45
|
+
error: {
|
|
46
|
+
code: "PROMOTION_MODEL_LIMIT_REACHED",
|
|
47
|
+
message: "Sign up for free to continue",
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
expect(parseSaeeolErrorCode(error)).toBe("PROMOTION_MODEL_LIMIT_REACHED")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("returns undefined for non-Saeeol error codes", () => {
|
|
55
|
+
const error = makeAPIError({
|
|
56
|
+
statusCode: 429,
|
|
57
|
+
responseBody: JSON.stringify({ error: { code: "SOME_OTHER_ERROR" } }),
|
|
58
|
+
})
|
|
59
|
+
expect(parseSaeeolErrorCode(error)).toBeUndefined()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("returns undefined for non-APIError types", () => {
|
|
63
|
+
const error = new MessageV2.AbortedError({ message: "aborted" }).toObject()
|
|
64
|
+
expect(parseSaeeolErrorCode(error)).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("returns undefined for malformed responseBody", () => {
|
|
68
|
+
const error = makeAPIError({
|
|
69
|
+
statusCode: 401,
|
|
70
|
+
responseBody: "not valid json",
|
|
71
|
+
})
|
|
72
|
+
expect(parseSaeeolErrorCode(error)).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("returns undefined when responseBody is missing", () => {
|
|
76
|
+
const error = makeAPIError({
|
|
77
|
+
statusCode: 401,
|
|
78
|
+
})
|
|
79
|
+
expect(parseSaeeolErrorCode(error)).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe("isSaeeolError", () => {
|
|
84
|
+
it("returns true for PAID_MODEL_AUTH_REQUIRED", () => {
|
|
85
|
+
const error = makeAPIError({
|
|
86
|
+
statusCode: 401,
|
|
87
|
+
responseBody: JSON.stringify({ error: { code: "PAID_MODEL_AUTH_REQUIRED" } }),
|
|
88
|
+
})
|
|
89
|
+
expect(isSaeeolError(error)).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("returns true for PROMOTION_MODEL_LIMIT_REACHED", () => {
|
|
93
|
+
const error = makeAPIError({
|
|
94
|
+
statusCode: 429,
|
|
95
|
+
responseBody: JSON.stringify({ code: "PROMOTION_MODEL_LIMIT_REACHED" }),
|
|
96
|
+
})
|
|
97
|
+
expect(isSaeeolError(error)).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("returns false for regular 429 errors without Saeeol code", () => {
|
|
101
|
+
const error = makeAPIError({
|
|
102
|
+
statusCode: 429,
|
|
103
|
+
isRetryable: true,
|
|
104
|
+
message: "Too Many Requests",
|
|
105
|
+
})
|
|
106
|
+
expect(isSaeeolError(error)).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("returns false for non-APIError types", () => {
|
|
110
|
+
const error = new MessageV2.AbortedError({ message: "aborted" }).toObject()
|
|
111
|
+
expect(isSaeeolError(error)).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe("SessionRetry.retryable with Saeeol errors", () => {
|
|
116
|
+
it("returns undefined for PAID_MODEL_AUTH_REQUIRED (not retryable)", () => {
|
|
117
|
+
const error = makeAPIError({
|
|
118
|
+
statusCode: 401,
|
|
119
|
+
isRetryable: false,
|
|
120
|
+
responseBody: JSON.stringify({ error: { code: "PAID_MODEL_AUTH_REQUIRED" } }),
|
|
121
|
+
})
|
|
122
|
+
expect(SessionRetry.retryable(error)).toBeUndefined()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("returns undefined for PROMOTION_MODEL_LIMIT_REACHED even when isRetryable is true", () => {
|
|
126
|
+
const error = makeAPIError({
|
|
127
|
+
statusCode: 429,
|
|
128
|
+
isRetryable: true,
|
|
129
|
+
responseBody: JSON.stringify({ code: "PROMOTION_MODEL_LIMIT_REACHED" }),
|
|
130
|
+
})
|
|
131
|
+
expect(SessionRetry.retryable(error)).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("still returns a string for regular 429 errors (retryable)", () => {
|
|
135
|
+
const error = makeAPIError({
|
|
136
|
+
statusCode: 429,
|
|
137
|
+
isRetryable: true,
|
|
138
|
+
message: "Too Many Requests",
|
|
139
|
+
})
|
|
140
|
+
const result = SessionRetry.retryable(error)
|
|
141
|
+
expect(result).toBeDefined()
|
|
142
|
+
expect(typeof result).toBe("string")
|
|
143
|
+
})
|
|
144
|
+
})
|