opencode-v2 1.1.53
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/AGENTS.md +27 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/opencode +84 -0
- package/bunfig.toml +5 -0
- package/package.json +126 -0
- package/parsers-config.ts +253 -0
- package/script/build.ts +193 -0
- package/script/postinstall.mjs +125 -0
- package/script/publish.ts +181 -0
- package/script/schema.ts +47 -0
- package/script/seed-e2e.ts +50 -0
- package/src/acp/README.md +164 -0
- package/src/acp/agent.ts +1676 -0
- package/src/acp/session.ts +117 -0
- package/src/acp/types.ts +23 -0
- package/src/agent/agent.ts +414 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/auth/index.ts +70 -0
- package/src/bun/index.ts +137 -0
- package/src/bun/registry.ts +48 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +70 -0
- package/src/cli/cmd/agent.ts +257 -0
- package/src/cli/cmd/auth.ts +400 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/debug/agent.ts +167 -0
- package/src/cli/cmd/debug/config.ts +16 -0
- package/src/cli/cmd/debug/file.ts +97 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +52 -0
- package/src/cli/cmd/debug/ripgrep.ts +87 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +16 -0
- package/src/cli/cmd/debug/snapshot.ts +52 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1540 -0
- package/src/cli/cmd/import.ts +147 -0
- package/src/cli/cmd/mcp.ts +755 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +617 -0
- package/src/cli/cmd/serve.ts +20 -0
- package/src/cli/cmd/session.ts +135 -0
- package/src/cli/cmd/stats.ts +426 -0
- package/src/cli/cmd/tui/app.tsx +801 -0
- package/src/cli/cmd/tui/attach.ts +52 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +148 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +234 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +266 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +177 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/logo.tsx +85 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +666 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +89 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1132 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/tips.tsx +153 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +15 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +52 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +100 -0
- package/src/cli/cmd/tui/context/kv.tsx +52 -0
- package/src/cli/cmd/tui/context/local.tsx +409 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +46 -0
- package/src/cli/cmd/tui/context/sdk.tsx +101 -0
- package/src/cli/cmd/tui/context/sync.tsx +470 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +249 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1152 -0
- package/src/cli/cmd/tui/event.ts +48 -0
- package/src/cli/cmd/tui/routes/home.tsx +140 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +142 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +2126 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +508 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +313 -0
- package/src/cli/cmd/tui/thread.ts +175 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +68 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +93 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +215 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +49 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +88 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +399 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +167 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +159 -0
- package/src/cli/cmd/tui/util/editor.ts +32 -0
- package/src/cli/cmd/tui/util/signal.ts +7 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/util/transcript.ts +98 -0
- package/src/cli/cmd/tui/worker.ts +152 -0
- package/src/cli/cmd/uninstall.ts +357 -0
- package/src/cli/cmd/upgrade.ts +73 -0
- package/src/cli/cmd/web.ts +81 -0
- package/src/cli/error.ts +57 -0
- package/src/cli/logo.ts +6 -0
- package/src/cli/network.ts +60 -0
- package/src/cli/ui.ts +113 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +150 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +99 -0
- package/src/config/config.ts +1477 -0
- package/src/config/markdown.ts +98 -0
- package/src/env/index.ts +28 -0
- package/src/file/ignore.ts +83 -0
- package/src/file/index.ts +583 -0
- package/src/file/ripgrep.ts +375 -0
- package/src/file/time.ts +69 -0
- package/src/file/watcher.ts +127 -0
- package/src/flag/flag.ts +97 -0
- package/src/format/formatter.ts +366 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +55 -0
- package/src/id/id.ts +83 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +159 -0
- package/src/installation/index.ts +246 -0
- package/src/lsp/client.ts +252 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +119 -0
- package/src/lsp/server.ts +2046 -0
- package/src/mcp/auth.ts +132 -0
- package/src/mcp/index.ts +934 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +154 -0
- package/src/patch/index.ts +680 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/index.ts +210 -0
- package/src/permission/next.ts +280 -0
- package/src/plugin/codex.ts +624 -0
- package/src/plugin/copilot.ts +327 -0
- package/src/plugin/index.ts +138 -0
- package/src/project/bootstrap.ts +35 -0
- package/src/project/instance.ts +114 -0
- package/src/project/project.ts +371 -0
- package/src/project/state.ts +70 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +147 -0
- package/src/provider/models.ts +133 -0
- package/src/provider/provider.ts +1262 -0
- package/src/provider/sdk/copilot/README.md +5 -0
- package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
- package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
- package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
- package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
- package/src/provider/sdk/copilot/index.ts +2 -0
- package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
- package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
- package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
- package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +828 -0
- package/src/pty/index.ts +250 -0
- package/src/question/index.ts +171 -0
- package/src/scheduler/index.ts +61 -0
- package/src/server/error.ts +36 -0
- package/src/server/event.ts +7 -0
- package/src/server/mdns.ts +60 -0
- package/src/server/routes/config.ts +92 -0
- package/src/server/routes/experimental.ts +208 -0
- package/src/server/routes/file.ts +197 -0
- package/src/server/routes/global.ts +183 -0
- package/src/server/routes/mcp.ts +225 -0
- package/src/server/routes/permission.ts +68 -0
- package/src/server/routes/project.ts +82 -0
- package/src/server/routes/provider.ts +165 -0
- package/src/server/routes/pty.ts +169 -0
- package/src/server/routes/question.ts +98 -0
- package/src/server/routes/session.ts +939 -0
- package/src/server/routes/tui.ts +379 -0
- package/src/server/server.ts +613 -0
- package/src/session/compaction.ts +226 -0
- package/src/session/index.ts +524 -0
- package/src/session/instruction.ts +197 -0
- package/src/session/llm.ts +289 -0
- package/src/session/message-v2.ts +802 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +407 -0
- package/src/session/prompt/agent.txt +43 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex_header.txt +79 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt/research.txt +81 -0
- package/src/session/prompt/trinity.txt +97 -0
- package/src/session/prompt.ts +1952 -0
- package/src/session/retry.ts +97 -0
- package/src/session/revert.ts +121 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +217 -0
- package/src/session/system.ts +54 -0
- package/src/session/todo.ts +37 -0
- package/src/share/share-next.ts +200 -0
- package/src/share/share.ts +92 -0
- package/src/shell/shell.ts +67 -0
- package/src/skill/discovery.ts +97 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +188 -0
- package/src/snapshot/index.ts +255 -0
- package/src/storage/storage.ts +227 -0
- package/src/tool/agent-enter.txt +1 -0
- package/src/tool/agent-exit.txt +1 -0
- package/src/tool/agent.ts +237 -0
- package/src/tool/apply_patch.ts +281 -0
- package/src/tool/apply_patch.txt +33 -0
- package/src/tool/bash.ts +269 -0
- package/src/tool/bash.txt +115 -0
- package/src/tool/batch.ts +175 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/chat-enter.txt +15 -0
- package/src/tool/chat-exit.txt +7 -0
- package/src/tool/chat.ts +217 -0
- package/src/tool/codesearch.ts +132 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +655 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/external-directory.ts +32 -0
- package/src/tool/glob.ts +78 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +147 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +121 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp.ts +96 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +130 -0
- package/src/tool/question.ts +33 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/read.ts +211 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +167 -0
- package/src/tool/research-enter.txt +1 -0
- package/src/tool/research-exit.txt +1 -0
- package/src/tool/research.ts +134 -0
- package/src/tool/skill.ts +123 -0
- package/src/tool/task.ts +165 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +53 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +89 -0
- package/src/tool/truncation.ts +106 -0
- package/src/tool/webfetch.ts +186 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +14 -0
- package/src/tool/write.ts +85 -0
- package/src/tool/write.txt +8 -0
- package/src/util/abort.ts +35 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +93 -0
- package/src/util/fn.ts +11 -0
- package/src/util/format.ts +20 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +103 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +180 -0
- package/src/util/proxied.ts +3 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +66 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +56 -0
- package/src/worktree/index.ts +574 -0
- package/sst-env.d.ts +9 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +436 -0
- package/test/agent/agent.test.ts +675 -0
- package/test/bun.test.ts +53 -0
- package/test/cli/github-action.test.ts +161 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/import.test.ts +38 -0
- package/test/cli/tui/transcript.test.ts +322 -0
- package/test/config/agent-color.test.ts +71 -0
- package/test/config/config.test.ts +1802 -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/markdown.test.ts +228 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/path-traversal.test.ts +198 -0
- package/test/file/ripgrep.test.ts +39 -0
- package/test/fixture/fixture.ts +45 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/keybind.test.ts +421 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/mcp/headers.test.ts +153 -0
- package/test/mcp/oauth-browser.test.ts +249 -0
- package/test/memory/abort-leak.test.ts +136 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +690 -0
- package/test/permission-task.test.ts +319 -0
- package/test/plugin/auth-override.test.ts +44 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/preload.ts +63 -0
- package/test/project/project.test.ts +120 -0
- package/test/provider/amazon-bedrock.test.ts +445 -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 +262 -0
- package/test/provider/provider.test.ts +2129 -0
- package/test/provider/transform.test.ts +2022 -0
- package/test/question/question.test.ts +300 -0
- package/test/scheduler.test.ts +73 -0
- package/test/server/session-list.test.ts +39 -0
- package/test/server/session-select.test.ts +78 -0
- package/test/session/compaction.test.ts +293 -0
- package/test/session/instruction.test.ts +170 -0
- package/test/session/llm.test.ts +691 -0
- package/test/session/message-v2.test.ts +786 -0
- package/test/session/prompt-missing-file.test.ts +53 -0
- package/test/session/prompt-special-chars.test.ts +56 -0
- package/test/session/prompt-variant.test.ts +60 -0
- package/test/session/retry.test.ts +179 -0
- package/test/session/revert-compact.test.ts +285 -0
- package/test/session/session.test.ts +71 -0
- package/test/skill/discovery.test.ts +60 -0
- package/test/skill/skill.test.ts +388 -0
- package/test/snapshot/snapshot.test.ts +1040 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +559 -0
- package/test/tool/bash.test.ts +399 -0
- package/test/tool/external-directory.test.ts +127 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +38413 -0
- package/test/tool/grep.test.ts +110 -0
- package/test/tool/question.test.ts +107 -0
- package/test/tool/read.test.ts +358 -0
- package/test/tool/registry.test.ts +122 -0
- package/test/tool/skill.test.ts +112 -0
- package/test/tool/truncation.test.ts +159 -0
- package/test/util/filesystem.test.ts +39 -0
- package/test/util/format.test.ts +59 -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/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +75 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { test, expect, mock, beforeEach } from "bun:test"
|
|
2
|
+
|
|
3
|
+
// Track what options were passed to each transport constructor
|
|
4
|
+
const transportCalls: Array<{
|
|
5
|
+
type: "streamable" | "sse"
|
|
6
|
+
url: string
|
|
7
|
+
options: { authProvider?: unknown; requestInit?: RequestInit }
|
|
8
|
+
}> = []
|
|
9
|
+
|
|
10
|
+
// Mock the transport constructors to capture their arguments
|
|
11
|
+
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
|
12
|
+
StreamableHTTPClientTransport: class MockStreamableHTTP {
|
|
13
|
+
constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
|
|
14
|
+
transportCalls.push({
|
|
15
|
+
type: "streamable",
|
|
16
|
+
url: url.toString(),
|
|
17
|
+
options: options ?? {},
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
async start() {
|
|
21
|
+
throw new Error("Mock transport cannot connect")
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
|
27
|
+
SSEClientTransport: class MockSSE {
|
|
28
|
+
constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
|
|
29
|
+
transportCalls.push({
|
|
30
|
+
type: "sse",
|
|
31
|
+
url: url.toString(),
|
|
32
|
+
options: options ?? {},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
async start() {
|
|
36
|
+
throw new Error("Mock transport cannot connect")
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
transportCalls.length = 0
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Import MCP after mocking
|
|
46
|
+
const { MCP } = await import("../../src/mcp/index")
|
|
47
|
+
const { Instance } = await import("../../src/project/instance")
|
|
48
|
+
const { tmpdir } = await import("../fixture/fixture")
|
|
49
|
+
|
|
50
|
+
test("headers are passed to transports when oauth is enabled (default)", async () => {
|
|
51
|
+
await using tmp = await tmpdir({
|
|
52
|
+
init: async (dir) => {
|
|
53
|
+
await Bun.write(
|
|
54
|
+
`${dir}/opencode.json`,
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
$schema: "https://opencode.ai/config.json",
|
|
57
|
+
mcp: {
|
|
58
|
+
"test-server": {
|
|
59
|
+
type: "remote",
|
|
60
|
+
url: "https://example.com/mcp",
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: "Bearer test-token",
|
|
63
|
+
"X-Custom-Header": "custom-value",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await Instance.provide({
|
|
73
|
+
directory: tmp.path,
|
|
74
|
+
fn: async () => {
|
|
75
|
+
// Trigger MCP initialization - it will fail to connect but we can check the transport options
|
|
76
|
+
await MCP.add("test-server", {
|
|
77
|
+
type: "remote",
|
|
78
|
+
url: "https://example.com/mcp",
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: "Bearer test-token",
|
|
81
|
+
"X-Custom-Header": "custom-value",
|
|
82
|
+
},
|
|
83
|
+
}).catch(() => {})
|
|
84
|
+
|
|
85
|
+
// Both transports should have been created with headers
|
|
86
|
+
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
|
|
87
|
+
|
|
88
|
+
for (const call of transportCalls) {
|
|
89
|
+
expect(call.options.requestInit).toBeDefined()
|
|
90
|
+
expect(call.options.requestInit?.headers).toEqual({
|
|
91
|
+
Authorization: "Bearer test-token",
|
|
92
|
+
"X-Custom-Header": "custom-value",
|
|
93
|
+
})
|
|
94
|
+
// OAuth should be enabled by default, so authProvider should exist
|
|
95
|
+
expect(call.options.authProvider).toBeDefined()
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test("headers are passed to transports when oauth is explicitly disabled", async () => {
|
|
102
|
+
await using tmp = await tmpdir()
|
|
103
|
+
|
|
104
|
+
await Instance.provide({
|
|
105
|
+
directory: tmp.path,
|
|
106
|
+
fn: async () => {
|
|
107
|
+
transportCalls.length = 0
|
|
108
|
+
|
|
109
|
+
await MCP.add("test-server-no-oauth", {
|
|
110
|
+
type: "remote",
|
|
111
|
+
url: "https://example.com/mcp",
|
|
112
|
+
oauth: false,
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: "Bearer test-token",
|
|
115
|
+
},
|
|
116
|
+
}).catch(() => {})
|
|
117
|
+
|
|
118
|
+
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
|
|
119
|
+
|
|
120
|
+
for (const call of transportCalls) {
|
|
121
|
+
expect(call.options.requestInit).toBeDefined()
|
|
122
|
+
expect(call.options.requestInit?.headers).toEqual({
|
|
123
|
+
Authorization: "Bearer test-token",
|
|
124
|
+
})
|
|
125
|
+
// OAuth is disabled, so no authProvider
|
|
126
|
+
expect(call.options.authProvider).toBeUndefined()
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("no requestInit when headers are not provided", async () => {
|
|
133
|
+
await using tmp = await tmpdir()
|
|
134
|
+
|
|
135
|
+
await Instance.provide({
|
|
136
|
+
directory: tmp.path,
|
|
137
|
+
fn: async () => {
|
|
138
|
+
transportCalls.length = 0
|
|
139
|
+
|
|
140
|
+
await MCP.add("test-server-no-headers", {
|
|
141
|
+
type: "remote",
|
|
142
|
+
url: "https://example.com/mcp",
|
|
143
|
+
}).catch(() => {})
|
|
144
|
+
|
|
145
|
+
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
|
|
146
|
+
|
|
147
|
+
for (const call of transportCalls) {
|
|
148
|
+
// No headers means requestInit should be undefined
|
|
149
|
+
expect(call.options.requestInit).toBeUndefined()
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { test, expect, mock, beforeEach } from "bun:test"
|
|
2
|
+
import { EventEmitter } from "events"
|
|
3
|
+
|
|
4
|
+
// Track open() calls and control failure behavior
|
|
5
|
+
let openShouldFail = false
|
|
6
|
+
let openCalledWith: string | undefined
|
|
7
|
+
|
|
8
|
+
mock.module("open", () => ({
|
|
9
|
+
default: async (url: string) => {
|
|
10
|
+
openCalledWith = url
|
|
11
|
+
|
|
12
|
+
// Return a mock subprocess that emits an error if openShouldFail is true
|
|
13
|
+
const subprocess = new EventEmitter()
|
|
14
|
+
if (openShouldFail) {
|
|
15
|
+
// Emit error asynchronously like a real subprocess would
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
subprocess.emit("error", new Error("spawn xdg-open ENOENT"))
|
|
18
|
+
}, 10)
|
|
19
|
+
}
|
|
20
|
+
return subprocess
|
|
21
|
+
},
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
// Mock UnauthorizedError
|
|
25
|
+
class MockUnauthorizedError extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super("Unauthorized")
|
|
28
|
+
this.name = "UnauthorizedError"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Track what options were passed to each transport constructor
|
|
33
|
+
const transportCalls: Array<{
|
|
34
|
+
type: "streamable" | "sse"
|
|
35
|
+
url: string
|
|
36
|
+
options: { authProvider?: unknown }
|
|
37
|
+
}> = []
|
|
38
|
+
|
|
39
|
+
// Mock the transport constructors
|
|
40
|
+
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
|
41
|
+
StreamableHTTPClientTransport: class MockStreamableHTTP {
|
|
42
|
+
url: string
|
|
43
|
+
authProvider: { redirectToAuthorization?: (url: URL) => Promise<void> } | undefined
|
|
44
|
+
constructor(url: URL, options?: { authProvider?: { redirectToAuthorization?: (url: URL) => Promise<void> } }) {
|
|
45
|
+
this.url = url.toString()
|
|
46
|
+
this.authProvider = options?.authProvider
|
|
47
|
+
transportCalls.push({
|
|
48
|
+
type: "streamable",
|
|
49
|
+
url: url.toString(),
|
|
50
|
+
options: options ?? {},
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
async start() {
|
|
54
|
+
// Simulate OAuth redirect by calling the authProvider's redirectToAuthorization
|
|
55
|
+
if (this.authProvider?.redirectToAuthorization) {
|
|
56
|
+
await this.authProvider.redirectToAuthorization(new URL("https://auth.example.com/authorize?client_id=test"))
|
|
57
|
+
}
|
|
58
|
+
throw new MockUnauthorizedError()
|
|
59
|
+
}
|
|
60
|
+
async finishAuth(_code: string) {
|
|
61
|
+
// Mock successful auth completion
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
|
67
|
+
SSEClientTransport: class MockSSE {
|
|
68
|
+
constructor(url: URL) {
|
|
69
|
+
transportCalls.push({
|
|
70
|
+
type: "sse",
|
|
71
|
+
url: url.toString(),
|
|
72
|
+
options: {},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
async start() {
|
|
76
|
+
throw new Error("Mock SSE transport cannot connect")
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
// Mock the MCP SDK Client to trigger OAuth flow
|
|
82
|
+
mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
83
|
+
Client: class MockClient {
|
|
84
|
+
async connect(transport: { start: () => Promise<void> }) {
|
|
85
|
+
await transport.start()
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
// Mock UnauthorizedError in the auth module
|
|
91
|
+
mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
|
92
|
+
UnauthorizedError: MockUnauthorizedError,
|
|
93
|
+
}))
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
openShouldFail = false
|
|
97
|
+
openCalledWith = undefined
|
|
98
|
+
transportCalls.length = 0
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Import modules after mocking
|
|
102
|
+
const { MCP } = await import("../../src/mcp/index")
|
|
103
|
+
const { Bus } = await import("../../src/bus")
|
|
104
|
+
const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
|
|
105
|
+
const { Instance } = await import("../../src/project/instance")
|
|
106
|
+
const { tmpdir } = await import("../fixture/fixture")
|
|
107
|
+
|
|
108
|
+
test("BrowserOpenFailed event is published when open() throws", async () => {
|
|
109
|
+
await using tmp = await tmpdir({
|
|
110
|
+
init: async (dir) => {
|
|
111
|
+
await Bun.write(
|
|
112
|
+
`${dir}/opencode.json`,
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
$schema: "https://opencode.ai/config.json",
|
|
115
|
+
mcp: {
|
|
116
|
+
"test-oauth-server": {
|
|
117
|
+
type: "remote",
|
|
118
|
+
url: "https://example.com/mcp",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
await Instance.provide({
|
|
127
|
+
directory: tmp.path,
|
|
128
|
+
fn: async () => {
|
|
129
|
+
openShouldFail = true
|
|
130
|
+
|
|
131
|
+
const events: Array<{ mcpName: string; url: string }> = []
|
|
132
|
+
const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
|
|
133
|
+
events.push(evt.properties)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Run authenticate with a timeout to avoid waiting forever for the callback
|
|
137
|
+
// Attach a handler immediately so callback shutdown rejections
|
|
138
|
+
// don't show up as unhandled between tests.
|
|
139
|
+
const authPromise = MCP.authenticate("test-oauth-server").catch(() => undefined)
|
|
140
|
+
|
|
141
|
+
// Config.get() can be slow in tests, so give it plenty of time.
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
|
143
|
+
|
|
144
|
+
// Stop the callback server and cancel any pending auth
|
|
145
|
+
await McpOAuthCallback.stop()
|
|
146
|
+
|
|
147
|
+
await authPromise
|
|
148
|
+
|
|
149
|
+
unsubscribe()
|
|
150
|
+
|
|
151
|
+
// Verify the BrowserOpenFailed event was published
|
|
152
|
+
expect(events.length).toBe(1)
|
|
153
|
+
expect(events[0].mcpName).toBe("test-oauth-server")
|
|
154
|
+
expect(events[0].url).toContain("https://")
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("BrowserOpenFailed event is NOT published when open() succeeds", async () => {
|
|
160
|
+
await using tmp = await tmpdir({
|
|
161
|
+
init: async (dir) => {
|
|
162
|
+
await Bun.write(
|
|
163
|
+
`${dir}/opencode.json`,
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
$schema: "https://opencode.ai/config.json",
|
|
166
|
+
mcp: {
|
|
167
|
+
"test-oauth-server-2": {
|
|
168
|
+
type: "remote",
|
|
169
|
+
url: "https://example.com/mcp",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await Instance.provide({
|
|
178
|
+
directory: tmp.path,
|
|
179
|
+
fn: async () => {
|
|
180
|
+
openShouldFail = false
|
|
181
|
+
|
|
182
|
+
const events: Array<{ mcpName: string; url: string }> = []
|
|
183
|
+
const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
|
|
184
|
+
events.push(evt.properties)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Run authenticate with a timeout to avoid waiting forever for the callback
|
|
188
|
+
const authPromise = MCP.authenticate("test-oauth-server-2").catch(() => undefined)
|
|
189
|
+
|
|
190
|
+
// Config.get() can be slow in tests; also covers the ~500ms open() error-detection window.
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
|
192
|
+
|
|
193
|
+
// Stop the callback server and cancel any pending auth
|
|
194
|
+
await McpOAuthCallback.stop()
|
|
195
|
+
|
|
196
|
+
await authPromise
|
|
197
|
+
|
|
198
|
+
unsubscribe()
|
|
199
|
+
|
|
200
|
+
// Verify NO BrowserOpenFailed event was published
|
|
201
|
+
expect(events.length).toBe(0)
|
|
202
|
+
// Verify open() was still called
|
|
203
|
+
expect(openCalledWith).toBeDefined()
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("open() is called with the authorization URL", async () => {
|
|
209
|
+
await using tmp = await tmpdir({
|
|
210
|
+
init: async (dir) => {
|
|
211
|
+
await Bun.write(
|
|
212
|
+
`${dir}/opencode.json`,
|
|
213
|
+
JSON.stringify({
|
|
214
|
+
$schema: "https://opencode.ai/config.json",
|
|
215
|
+
mcp: {
|
|
216
|
+
"test-oauth-server-3": {
|
|
217
|
+
type: "remote",
|
|
218
|
+
url: "https://example.com/mcp",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
await Instance.provide({
|
|
227
|
+
directory: tmp.path,
|
|
228
|
+
fn: async () => {
|
|
229
|
+
openShouldFail = false
|
|
230
|
+
openCalledWith = undefined
|
|
231
|
+
|
|
232
|
+
// Run authenticate with a timeout to avoid waiting forever for the callback
|
|
233
|
+
const authPromise = MCP.authenticate("test-oauth-server-3").catch(() => undefined)
|
|
234
|
+
|
|
235
|
+
// Config.get() can be slow in tests; also covers the ~500ms open() error-detection window.
|
|
236
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
|
237
|
+
|
|
238
|
+
// Stop the callback server and cancel any pending auth
|
|
239
|
+
await McpOAuthCallback.stop()
|
|
240
|
+
|
|
241
|
+
await authPromise
|
|
242
|
+
|
|
243
|
+
// Verify open was called with a URL
|
|
244
|
+
expect(openCalledWith).toBeDefined()
|
|
245
|
+
expect(typeof openCalledWith).toBe("string")
|
|
246
|
+
expect(openCalledWith!).toContain("https://")
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { Instance } from "../../src/project/instance"
|
|
4
|
+
import { WebFetchTool } from "../../src/tool/webfetch"
|
|
5
|
+
|
|
6
|
+
const projectRoot = path.join(__dirname, "../..")
|
|
7
|
+
|
|
8
|
+
const ctx = {
|
|
9
|
+
sessionID: "test",
|
|
10
|
+
messageID: "",
|
|
11
|
+
callID: "",
|
|
12
|
+
agent: "build",
|
|
13
|
+
abort: new AbortController().signal,
|
|
14
|
+
messages: [],
|
|
15
|
+
metadata: () => {},
|
|
16
|
+
ask: async () => {},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MB = 1024 * 1024
|
|
20
|
+
const ITERATIONS = 50
|
|
21
|
+
|
|
22
|
+
const getHeapMB = () => {
|
|
23
|
+
Bun.gc(true)
|
|
24
|
+
return process.memoryUsage().heapUsed / MB
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("memory: abort controller leak", () => {
|
|
28
|
+
test("webfetch does not leak memory over many invocations", async () => {
|
|
29
|
+
await Instance.provide({
|
|
30
|
+
directory: projectRoot,
|
|
31
|
+
fn: async () => {
|
|
32
|
+
const tool = await WebFetchTool.init()
|
|
33
|
+
|
|
34
|
+
// Warm up
|
|
35
|
+
await tool.execute({ url: "https://example.com", format: "text" }, ctx).catch(() => {})
|
|
36
|
+
|
|
37
|
+
Bun.gc(true)
|
|
38
|
+
const baseline = getHeapMB()
|
|
39
|
+
|
|
40
|
+
// Run many fetches
|
|
41
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
42
|
+
await tool.execute({ url: "https://example.com", format: "text" }, ctx).catch(() => {})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Bun.gc(true)
|
|
46
|
+
const after = getHeapMB()
|
|
47
|
+
const growth = after - baseline
|
|
48
|
+
|
|
49
|
+
console.log(`Baseline: ${baseline.toFixed(2)} MB`)
|
|
50
|
+
console.log(`After ${ITERATIONS} fetches: ${after.toFixed(2)} MB`)
|
|
51
|
+
console.log(`Growth: ${growth.toFixed(2)} MB`)
|
|
52
|
+
|
|
53
|
+
// Memory growth should be minimal - less than 1MB per 10 requests
|
|
54
|
+
// With the old closure pattern, this would grow ~0.5MB per request
|
|
55
|
+
expect(growth).toBeLessThan(ITERATIONS / 10)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}, 60000)
|
|
59
|
+
|
|
60
|
+
test("compare closure vs bind pattern directly", async () => {
|
|
61
|
+
const ITERATIONS = 500
|
|
62
|
+
|
|
63
|
+
// Test OLD pattern: arrow function closure
|
|
64
|
+
// Store closures in a map keyed by content to force retention
|
|
65
|
+
const closureMap = new Map<string, () => void>()
|
|
66
|
+
const timers: Timer[] = []
|
|
67
|
+
const controllers: AbortController[] = []
|
|
68
|
+
|
|
69
|
+
Bun.gc(true)
|
|
70
|
+
Bun.sleepSync(100)
|
|
71
|
+
const baseline = getHeapMB()
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
74
|
+
// Simulate large response body like webfetch would have
|
|
75
|
+
const content = `${i}:${"x".repeat(50 * 1024)}` // 50KB unique per iteration
|
|
76
|
+
const controller = new AbortController()
|
|
77
|
+
controllers.push(controller)
|
|
78
|
+
|
|
79
|
+
// OLD pattern - closure captures `content`
|
|
80
|
+
const handler = () => {
|
|
81
|
+
// Actually use content so it can't be optimized away
|
|
82
|
+
if (content.length > 1000000000) controller.abort()
|
|
83
|
+
}
|
|
84
|
+
closureMap.set(content, handler)
|
|
85
|
+
const timeoutId = setTimeout(handler, 30000)
|
|
86
|
+
timers.push(timeoutId)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Bun.gc(true)
|
|
90
|
+
Bun.sleepSync(100)
|
|
91
|
+
const after = getHeapMB()
|
|
92
|
+
const oldGrowth = after - baseline
|
|
93
|
+
|
|
94
|
+
console.log(`OLD pattern (closure): ${oldGrowth.toFixed(2)} MB growth (${closureMap.size} closures)`)
|
|
95
|
+
|
|
96
|
+
// Cleanup after measuring
|
|
97
|
+
timers.forEach(clearTimeout)
|
|
98
|
+
controllers.forEach((c) => c.abort())
|
|
99
|
+
closureMap.clear()
|
|
100
|
+
|
|
101
|
+
// Test NEW pattern: bind
|
|
102
|
+
Bun.gc(true)
|
|
103
|
+
Bun.sleepSync(100)
|
|
104
|
+
const baseline2 = getHeapMB()
|
|
105
|
+
const handlers2: (() => void)[] = []
|
|
106
|
+
const timers2: Timer[] = []
|
|
107
|
+
const controllers2: AbortController[] = []
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
110
|
+
const _content = `${i}:${"x".repeat(50 * 1024)}` // 50KB - won't be captured
|
|
111
|
+
const controller = new AbortController()
|
|
112
|
+
controllers2.push(controller)
|
|
113
|
+
|
|
114
|
+
// NEW pattern - bind doesn't capture surrounding scope
|
|
115
|
+
const handler = controller.abort.bind(controller)
|
|
116
|
+
handlers2.push(handler)
|
|
117
|
+
const timeoutId = setTimeout(handler, 30000)
|
|
118
|
+
timers2.push(timeoutId)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Bun.gc(true)
|
|
122
|
+
Bun.sleepSync(100)
|
|
123
|
+
const after2 = getHeapMB()
|
|
124
|
+
const newGrowth = after2 - baseline2
|
|
125
|
+
|
|
126
|
+
// Cleanup after measuring
|
|
127
|
+
timers2.forEach(clearTimeout)
|
|
128
|
+
controllers2.forEach((c) => c.abort())
|
|
129
|
+
handlers2.length = 0
|
|
130
|
+
|
|
131
|
+
console.log(`NEW pattern (bind): ${newGrowth.toFixed(2)} MB growth`)
|
|
132
|
+
console.log(`Improvement: ${(oldGrowth - newGrowth).toFixed(2)} MB saved`)
|
|
133
|
+
|
|
134
|
+
expect(newGrowth).toBeLessThanOrEqual(oldGrowth)
|
|
135
|
+
})
|
|
136
|
+
})
|