@alumbwe/anvil 1.0.19 → 1.0.20
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/README.md +28 -30
- package/bin/anvil +2 -0
- package/package.json +71 -75
- package/src/agents/bundled-agents.generated.d.ts +14 -0
- package/src/agents/bundled-agents.generated.ts +3760 -0
- package/src/app.tsx +399 -0
- package/src/chat.tsx +1698 -0
- package/src/cli-args.ts +134 -0
- package/src/commands/__tests__/anvil-command-aliases.test.ts +56 -0
- package/src/commands/__tests__/bash-command.test.ts +445 -0
- package/src/commands/__tests__/command-args.test.ts +349 -0
- package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
- package/src/commands/__tests__/copy-conversation.test.ts +226 -0
- package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
- package/src/commands/__tests__/image.test.ts +99 -0
- package/src/commands/__tests__/init.test.ts +456 -0
- package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
- package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
- package/src/commands/__tests__/prompt-builders.test.ts +28 -0
- package/src/commands/__tests__/router-input.test.ts +298 -0
- package/src/commands/ads.ts +44 -0
- package/src/commands/command-registry.ts +726 -0
- package/src/commands/copy-conversation.ts +392 -0
- package/src/commands/help.ts +13 -0
- package/src/commands/image.ts +20 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/process-diagnostics.ts +120 -0
- package/src/commands/prompt-builders.ts +100 -0
- package/src/commands/publish.ts +203 -0
- package/src/commands/router-utils.ts +80 -0
- package/src/commands/router.ts +487 -0
- package/src/commands/usage.ts +26 -0
- package/src/components/__tests__/ad-banner.test.tsx +83 -0
- package/src/components/__tests__/anvil-landing-heading-row.test.tsx +91 -0
- package/src/components/__tests__/anvil-model-selector.test.tsx +519 -0
- package/src/components/__tests__/anvil-offer-invariants.test.ts +54 -0
- package/src/components/__tests__/anvil-takeover-prompt.test.tsx +212 -0
- package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
- package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
- package/src/components/__tests__/grid-layout.test.tsx +1051 -0
- package/src/components/__tests__/message-block.completion.test.tsx +104 -0
- package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
- package/src/components/__tests__/message-with-agents.test.tsx +579 -0
- package/src/components/__tests__/multiline-input.test.tsx +1132 -0
- package/src/components/__tests__/queue-panel.test.tsx +398 -0
- package/src/components/__tests__/selectable-list.test.ts +232 -0
- package/src/components/__tests__/status-bar.test.tsx +44 -0
- package/src/components/__tests__/status-indicator.test.tsx +204 -0
- package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
- package/src/components/__tests__/user-error-banner.test.tsx +102 -0
- package/src/components/ad-banner.tsx +323 -0
- package/src/components/agent-checklist.tsx +350 -0
- package/src/components/agent-mode-toggle.tsx +241 -0
- package/src/components/anvil-active-session-summary.tsx +63 -0
- package/src/components/anvil-landing-screen.tsx +865 -0
- package/src/components/anvil-mark.tsx +52 -0
- package/src/components/anvil-model-selector.tsx +1087 -0
- package/src/components/anvil-referral-banner.tsx +604 -0
- package/src/components/anvil-superseded-screen.tsx +62 -0
- package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
- package/src/components/ask-user/__tests__/validation.test.ts +213 -0
- package/src/components/ask-user/components/accordion-question.tsx +153 -0
- package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
- package/src/components/ask-user/components/options-list.tsx +133 -0
- package/src/components/ask-user/components/question-header.tsx +80 -0
- package/src/components/ask-user/components/question-option.tsx +89 -0
- package/src/components/ask-user/constants.ts +38 -0
- package/src/components/ask-user/index.tsx +631 -0
- package/src/components/ask-user/utils/validation.ts +85 -0
- package/src/components/attachment-card.tsx +65 -0
- package/src/components/blocks/agent-block-grid.tsx +58 -0
- package/src/components/blocks/agent-branch-item.tsx +311 -0
- package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
- package/src/components/blocks/agent-list-branch.tsx +77 -0
- package/src/components/blocks/ask-user-branch.tsx +81 -0
- package/src/components/blocks/block-helpers.ts +11 -0
- package/src/components/blocks/blocks-renderer.tsx +289 -0
- package/src/components/blocks/content-with-markdown.tsx +33 -0
- package/src/components/blocks/image-block.tsx +128 -0
- package/src/components/blocks/implementor-row.tsx +429 -0
- package/src/components/blocks/single-block.tsx +201 -0
- package/src/components/blocks/thinking-block.tsx +67 -0
- package/src/components/blocks/tool-block-group.tsx +57 -0
- package/src/components/blocks/tool-branch.tsx +206 -0
- package/src/components/blocks/user-content-copy.tsx +157 -0
- package/src/components/bottom-banner.tsx +136 -0
- package/src/components/build-mode-buttons.tsx +110 -0
- package/src/components/button.tsx +66 -0
- package/src/components/chat-header.tsx +77 -0
- package/src/components/chat-history-screen.tsx +413 -0
- package/src/components/chat-input-bar.tsx +523 -0
- package/src/components/clickable-title-box.tsx +66 -0
- package/src/components/clickable.tsx +119 -0
- package/src/components/collapse-button.tsx +34 -0
- package/src/components/copy-button.tsx +203 -0
- package/src/components/elapsed-timer.tsx +55 -0
- package/src/components/error-boundary.tsx +55 -0
- package/src/components/feedback-container.tsx +181 -0
- package/src/components/feedback-icon-button.tsx +89 -0
- package/src/components/feedback-input-mode.tsx +344 -0
- package/src/components/file-attachment-card.tsx +98 -0
- package/src/components/grid-layout.tsx +80 -0
- package/src/components/help-banner.tsx +127 -0
- package/src/components/highlighted-text.tsx +52 -0
- package/src/components/image-card.tsx +167 -0
- package/src/components/image-thumbnail.tsx +114 -0
- package/src/components/input-cursor.tsx +77 -0
- package/src/components/input-mode-banner.tsx +57 -0
- package/src/components/load-previous-button.tsx +48 -0
- package/src/components/login-modal.tsx +475 -0
- package/src/components/message-block.tsx +346 -0
- package/src/components/message-footer.tsx +258 -0
- package/src/components/message-with-agents.tsx +506 -0
- package/src/components/mode-divider.tsx +43 -0
- package/src/components/multiline-input.tsx +1241 -0
- package/src/components/out-of-credits-banner.tsx +160 -0
- package/src/components/pending-attachments-banner.tsx +116 -0
- package/src/components/pending-bash-message.tsx +59 -0
- package/src/components/progress-bar.tsx +81 -0
- package/src/components/project-picker-screen.tsx +485 -0
- package/src/components/publish-confirmation.tsx +493 -0
- package/src/components/publish-container.tsx +687 -0
- package/src/components/queue-panel.tsx +310 -0
- package/src/components/raised-pill.tsx +95 -0
- package/src/components/renderers/plan-box.tsx +59 -0
- package/src/components/review-screen.tsx +112 -0
- package/src/components/scroll-to-bottom-button.tsx +34 -0
- package/src/components/segmented-control.tsx +187 -0
- package/src/components/selectable-list.tsx +248 -0
- package/src/components/selected-chips.tsx +80 -0
- package/src/components/separator.tsx +40 -0
- package/src/components/session-ended-banner.tsx +226 -0
- package/src/components/shimmer-text.tsx +248 -0
- package/src/components/status-bar.tsx +313 -0
- package/src/components/subscription-limit-banner.tsx +206 -0
- package/src/components/suggested-prompts.tsx +184 -0
- package/src/components/suggestion-menu.tsx +215 -0
- package/src/components/terminal-command-display.tsx +151 -0
- package/src/components/terminal-link.tsx +97 -0
- package/src/components/terminal-thinking-orb.tsx +95 -0
- package/src/components/text-attachment-card.tsx +77 -0
- package/src/components/thinking.tsx +109 -0
- package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
- package/src/components/tools/__tests__/code-search.test.tsx +45 -0
- package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
- package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
- package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
- package/src/components/tools/apply-patch.tsx +95 -0
- package/src/components/tools/code-search.tsx +45 -0
- package/src/components/tools/composio.tsx +152 -0
- package/src/components/tools/diff-viewer.tsx +76 -0
- package/src/components/tools/glob.tsx +61 -0
- package/src/components/tools/gravity-index.tsx +77 -0
- package/src/components/tools/list-directory.tsx +61 -0
- package/src/components/tools/read-docs.tsx +37 -0
- package/src/components/tools/read-files.tsx +91 -0
- package/src/components/tools/read-subtree.tsx +44 -0
- package/src/components/tools/read-url.tsx +33 -0
- package/src/components/tools/registry.ts +126 -0
- package/src/components/tools/render-ui.tsx +122 -0
- package/src/components/tools/run-terminal-command.tsx +79 -0
- package/src/components/tools/skill.tsx +29 -0
- package/src/components/tools/str-replace.tsx +77 -0
- package/src/components/tools/suggest-followups.tsx +369 -0
- package/src/components/tools/task-completed.tsx +16 -0
- package/src/components/tools/tool-call-item.tsx +273 -0
- package/src/components/tools/types.ts +55 -0
- package/src/components/tools/web-search.tsx +33 -0
- package/src/components/tools/write-file.tsx +16 -0
- package/src/components/tools/write-todos.tsx +95 -0
- package/src/components/top-banner.tsx +178 -0
- package/src/components/usage-banner.tsx +244 -0
- package/src/components/user-error-banner.tsx +56 -0
- package/src/components/validation-error-popover.tsx +192 -0
- package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
- package/src/contexts/chat-runtime-context.tsx +240 -0
- package/src/data/slash-commands.test.ts +24 -0
- package/src/data/slash-commands.ts +249 -0
- package/src/entry.ts +12 -0
- package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +49 -0
- package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
- package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
- package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
- package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
- package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
- package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
- package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
- package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
- package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
- package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
- package/src/hooks/__tests__/use-input-history.test.ts +699 -0
- package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
- package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
- package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
- package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
- package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
- package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
- package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
- package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
- package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
- package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
- package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
- package/src/hooks/__tests__/use-timeout.test.ts +330 -0
- package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
- package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
- package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
- package/src/hooks/helpers/send-message.ts +654 -0
- package/src/hooks/stream-state.ts +83 -0
- package/src/hooks/use-activity-query.ts +668 -0
- package/src/hooks/use-agent-validation.ts +74 -0
- package/src/hooks/use-anvil-ctrl-c-exit.ts +23 -0
- package/src/hooks/use-anvil-session-progress.ts +34 -0
- package/src/hooks/use-anvil-session.ts +806 -0
- package/src/hooks/use-anvil-streak-query.ts +61 -0
- package/src/hooks/use-ask-user-bridge.ts +92 -0
- package/src/hooks/use-auth-query.ts +262 -0
- package/src/hooks/use-auth-state.ts +152 -0
- package/src/hooks/use-chat-input.ts +105 -0
- package/src/hooks/use-chat-keyboard.ts +321 -0
- package/src/hooks/use-chat-messages.ts +254 -0
- package/src/hooks/use-chat-state.ts +165 -0
- package/src/hooks/use-chat-streaming.ts +180 -0
- package/src/hooks/use-chat-ui.ts +131 -0
- package/src/hooks/use-clipboard.ts +151 -0
- package/src/hooks/use-connection-status.ts +142 -0
- package/src/hooks/use-directory-browser.ts +88 -0
- package/src/hooks/use-elapsed-time.ts +116 -0
- package/src/hooks/use-event.ts +34 -0
- package/src/hooks/use-exit-handler.ts +66 -0
- package/src/hooks/use-fetch-login-url.ts +65 -0
- package/src/hooks/use-fingerprint.ts +61 -0
- package/src/hooks/use-gravity-ad.ts +656 -0
- package/src/hooks/use-grid-layout.ts +73 -0
- package/src/hooks/use-input-history.ts +173 -0
- package/src/hooks/use-login-keyboard-handlers.ts +75 -0
- package/src/hooks/use-login-polling.ts +114 -0
- package/src/hooks/use-logo.tsx +161 -0
- package/src/hooks/use-message-queue.ts +387 -0
- package/src/hooks/use-now.ts +20 -0
- package/src/hooks/use-path-tab-completion.ts +88 -0
- package/src/hooks/use-publish-mutation.ts +60 -0
- package/src/hooks/use-queue-controls.ts +68 -0
- package/src/hooks/use-queue-ui.ts +74 -0
- package/src/hooks/use-scroll-management.ts +184 -0
- package/src/hooks/use-searchable-list.ts +98 -0
- package/src/hooks/use-send-message.ts +773 -0
- package/src/hooks/use-sheen-animation.tsx +88 -0
- package/src/hooks/use-subscription-query.ts +71 -0
- package/src/hooks/use-suggestion-engine.ts +783 -0
- package/src/hooks/use-terminal-breakpoints.ts +77 -0
- package/src/hooks/use-terminal-dimensions.ts +46 -0
- package/src/hooks/use-terminal-focus.ts +29 -0
- package/src/hooks/use-terminal-layout.ts +189 -0
- package/src/hooks/use-theme.tsx +140 -0
- package/src/hooks/use-timeout.ts +85 -0
- package/src/hooks/use-update-preference.ts +66 -0
- package/src/hooks/use-usage-monitor.ts +55 -0
- package/src/hooks/use-usage-query.ts +124 -0
- package/src/hooks/use-user-details-query.ts +93 -0
- package/src/hooks/use-why-did-you-update.ts +177 -0
- package/src/index.tsx +473 -0
- package/src/init/__tests__/init-direnv.test.ts +526 -0
- package/src/init/init-app.ts +34 -0
- package/src/init/init-direnv.ts +133 -0
- package/src/login/constants.ts +60 -0
- package/src/login/login-flow.ts +204 -0
- package/src/login/plain-login.ts +108 -0
- package/src/login/utils.ts +183 -0
- package/src/native/ripgrep.ts +80 -0
- package/src/polyfills/bun-strip-ansi.ts +12 -0
- package/src/pre-init/tree-sitter-wasm.ts +96 -0
- package/src/project-files.ts +102 -0
- package/src/setup/key-setup.ts +295 -0
- package/src/setup/load-env.ts +106 -0
- package/src/state/__tests__/chat-store-focus.test.ts +22 -0
- package/src/state/__tests__/feedback-store.test.ts +257 -0
- package/src/state/anvil-model-store.ts +42 -0
- package/src/state/anvil-session-store.ts +53 -0
- package/src/state/chat-history-store.ts +30 -0
- package/src/state/chat-store.ts +544 -0
- package/src/state/feedback-store.ts +160 -0
- package/src/state/login-store.ts +150 -0
- package/src/state/message-block-store.ts +149 -0
- package/src/state/publish-store.ts +146 -0
- package/src/state/queue-panel-store.ts +26 -0
- package/src/state/review-store.ts +24 -0
- package/src/types/anvil-session.ts +17 -0
- package/src/types/chat-state.ts +15 -0
- package/src/types/chat.ts +241 -0
- package/src/types/contracts/send-message.ts +12 -0
- package/src/types/env.ts +98 -0
- package/src/types/function-params.ts +28 -0
- package/src/types/react19-compat.d.ts +19 -0
- package/src/types/store.ts +111 -0
- package/src/types/theme-system.ts +144 -0
- package/src/types/utils.ts +3 -0
- package/src/utils/__tests__/active-run.test.ts +140 -0
- package/src/utils/__tests__/activity-tracker.test.ts +440 -0
- package/src/utils/__tests__/agent-display.test.ts +139 -0
- package/src/utils/__tests__/analytics-client.test.ts +262 -0
- package/src/utils/__tests__/anonymous-id.test.ts +74 -0
- package/src/utils/__tests__/anvil-api.test.ts +578 -0
- package/src/utils/__tests__/anvil-instance-owner.test.ts +69 -0
- package/src/utils/__tests__/anvil-model-navigation.test.ts +102 -0
- package/src/utils/__tests__/anvil-premium-reset.test.ts +79 -0
- package/src/utils/__tests__/anvil-referral-cache.test.ts +116 -0
- package/src/utils/__tests__/anvil-session-api.test.ts +215 -0
- package/src/utils/__tests__/anvil-session-display.test.ts +23 -0
- package/src/utils/__tests__/anvil-streak-line.test.ts +217 -0
- package/src/utils/__tests__/arrays.test.ts +264 -0
- package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
- package/src/utils/__tests__/block-processor.test.ts +810 -0
- package/src/utils/__tests__/chat-history.test.ts +203 -0
- package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
- package/src/utils/__tests__/chat-meta.test.ts +130 -0
- package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
- package/src/utils/__tests__/clipboard.test.ts +910 -0
- package/src/utils/__tests__/code-search-summary.test.ts +84 -0
- package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
- package/src/utils/__tests__/error-handling.test.ts +658 -0
- package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
- package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
- package/src/utils/__tests__/feedback-submission.test.ts +26 -0
- package/src/utils/__tests__/fetch-usage.test.ts +355 -0
- package/src/utils/__tests__/fingerprint.test.ts +144 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
- package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
- package/src/utils/__tests__/format-timeout.test.ts +87 -0
- package/src/utils/__tests__/image-dimensions.test.ts +249 -0
- package/src/utils/__tests__/image-processor.test.ts +209 -0
- package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
- package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
- package/src/utils/__tests__/layout-helpers.test.ts +36 -0
- package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
- package/src/utils/__tests__/log-shipper.test.ts +53 -0
- package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
- package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
- package/src/utils/__tests__/message-updater.test.ts +645 -0
- package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
- package/src/utils/__tests__/pending-attachments.test.ts +277 -0
- package/src/utils/__tests__/polling-backoff.test.ts +51 -0
- package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
- package/src/utils/__tests__/run-state-storage.test.ts +919 -0
- package/src/utils/__tests__/safe-json.test.ts +77 -0
- package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
- package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
- package/src/utils/__tests__/send-message-timer.test.ts +64 -0
- package/src/utils/__tests__/settings.test.ts +99 -0
- package/src/utils/__tests__/strings.test.ts +192 -0
- package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
- package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
- package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
- package/src/utils/__tests__/terminal-io.test.ts +137 -0
- package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
- package/src/utils/__tests__/text-layout.test.ts +82 -0
- package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
- package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
- package/src/utils/__tests__/trace-writer.test.ts +223 -0
- package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
- package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
- package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
- package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
- package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
- package/src/utils/active-run.ts +131 -0
- package/src/utils/activity-tracker.ts +66 -0
- package/src/utils/agent-display.ts +87 -0
- package/src/utils/agent-helpers.ts +27 -0
- package/src/utils/agent-id-utils.ts +14 -0
- package/src/utils/ai-message-id.ts +12 -0
- package/src/utils/analytics.ts +361 -0
- package/src/utils/anonymous-id.ts +87 -0
- package/src/utils/anvil-agent-selection.ts +33 -0
- package/src/utils/anvil-api.ts +612 -0
- package/src/utils/anvil-client.ts +217 -0
- package/src/utils/anvil-instance-owner.ts +66 -0
- package/src/utils/anvil-model-navigation.ts +50 -0
- package/src/utils/anvil-premium-reset.ts +55 -0
- package/src/utils/anvil-referral-cache.ts +51 -0
- package/src/utils/anvil-session-api.ts +243 -0
- package/src/utils/anvil-session-display.ts +21 -0
- package/src/utils/anvil-streak-line.ts +93 -0
- package/src/utils/arrays.ts +117 -0
- package/src/utils/auth.ts +258 -0
- package/src/utils/bash-context-processor.ts +47 -0
- package/src/utils/bash-messages.ts +108 -0
- package/src/utils/block-margins.ts +35 -0
- package/src/utils/block-operations.ts +523 -0
- package/src/utils/block-processor.ts +213 -0
- package/src/utils/chat-history.ts +253 -0
- package/src/utils/chat-input-key-intercept.ts +52 -0
- package/src/utils/chat-meta.ts +90 -0
- package/src/utils/chat-scroll-accel.ts +151 -0
- package/src/utils/clipboard-image.ts +588 -0
- package/src/utils/clipboard.ts +397 -0
- package/src/utils/code-search-summary.ts +70 -0
- package/src/utils/collapse-helpers.ts +256 -0
- package/src/utils/config-dir.ts +24 -0
- package/src/utils/constants.ts +190 -0
- package/src/utils/create-event-handler-state.ts +73 -0
- package/src/utils/create-run-config.ts +121 -0
- package/src/utils/detect-shell.ts +116 -0
- package/src/utils/directory-browser.ts +72 -0
- package/src/utils/engagement.ts +55 -0
- package/src/utils/env.ts +92 -0
- package/src/utils/error-handling.ts +255 -0
- package/src/utils/error-messages.ts +62 -0
- package/src/utils/exit-cleanly.ts +86 -0
- package/src/utils/feedback-helpers.ts +103 -0
- package/src/utils/feedback-submission.ts +22 -0
- package/src/utils/fetch-usage.ts +76 -0
- package/src/utils/fingerprint.ts +245 -0
- package/src/utils/format-elapsed-time.ts +33 -0
- package/src/utils/format-session-units.ts +6 -0
- package/src/utils/format-timeout.ts +28 -0
- package/src/utils/format-validation-errors-for-message.ts +84 -0
- package/src/utils/git.ts +17 -0
- package/src/utils/helpers.ts +47 -0
- package/src/utils/image-display.ts +67 -0
- package/src/utils/image-handler.ts +325 -0
- package/src/utils/image-processor.ts +133 -0
- package/src/utils/image-thumbnail.ts +78 -0
- package/src/utils/implementor-helpers.ts +787 -0
- package/src/utils/input-modes.ts +174 -0
- package/src/utils/keyboard-actions.ts +392 -0
- package/src/utils/keypad-keys.ts +47 -0
- package/src/utils/layout-helpers.ts +33 -0
- package/src/utils/lazy-response-ads.ts +1 -0
- package/src/utils/local-agent-registry.ts +474 -0
- package/src/utils/log-shipper.ts +126 -0
- package/src/utils/logger.ts +327 -0
- package/src/utils/markdown-renderer.tsx +1090 -0
- package/src/utils/math.ts +3 -0
- package/src/utils/message-block-helpers.ts +684 -0
- package/src/utils/message-history.ts +143 -0
- package/src/utils/message-tree-utils.ts +67 -0
- package/src/utils/message-updater.ts +279 -0
- package/src/utils/open-file.ts +146 -0
- package/src/utils/open-url.ts +63 -0
- package/src/utils/path-completion.ts +111 -0
- package/src/utils/path-helpers.ts +38 -0
- package/src/utils/pending-attachments.ts +360 -0
- package/src/utils/polling-backoff.ts +59 -0
- package/src/utils/project-picker.ts +12 -0
- package/src/utils/queue-panel-actions.ts +70 -0
- package/src/utils/recent-projects.ts +156 -0
- package/src/utils/renderer-cleanup.ts +233 -0
- package/src/utils/response-ad-positions.ts +1 -0
- package/src/utils/run-state-storage.ts +603 -0
- package/src/utils/safe-json.ts +153 -0
- package/src/utils/sdk-event-handlers.ts +571 -0
- package/src/utils/send-message-helpers.ts +210 -0
- package/src/utils/send-message-timer.ts +110 -0
- package/src/utils/settings.ts +226 -0
- package/src/utils/skill-registry.ts +94 -0
- package/src/utils/spawn-agent-matcher.ts +55 -0
- package/src/utils/status-indicator-state.ts +107 -0
- package/src/utils/stream-chunk-processor.ts +57 -0
- package/src/utils/strings.ts +271 -0
- package/src/utils/subscription.ts +31 -0
- package/src/utils/syntax-highlighter.tsx +19 -0
- package/src/utils/terminal-color-detection.ts +474 -0
- package/src/utils/terminal-command-broker.ts +467 -0
- package/src/utils/terminal-enter-detection.ts +63 -0
- package/src/utils/terminal-images.ts +229 -0
- package/src/utils/terminal-io.ts +85 -0
- package/src/utils/terminal-protocol-controller.ts +173 -0
- package/src/utils/terminal-reset-sequences.ts +33 -0
- package/src/utils/terminal-title.ts +84 -0
- package/src/utils/terminal-watchdog.ts +366 -0
- package/src/utils/text-layout.ts +149 -0
- package/src/utils/theme-config.ts +141 -0
- package/src/utils/theme-system.ts +1206 -0
- package/src/utils/think-tag-parser.ts +95 -0
- package/src/utils/time-format.ts +21 -0
- package/src/utils/trace-writer.ts +162 -0
- package/src/utils/ui-constants.ts +69 -0
- package/src/utils/update-checker.ts +110 -0
- package/src/utils/usage-banner-state.ts +146 -0
- package/src/utils/validation-error-formatting.ts +86 -0
- package/src/utils/validation-error-helpers.ts +19 -0
- package/src/utils/windows-terminal-health.ts +124 -0
- package/src/utils/word-wrap-utils.ts +53 -0
- package/src/utils/write-file-atomic.ts +54 -0
- package/src/utils/yield-to-event-loop.ts +9 -0
- package/bin/anvil.cjs +0 -40
- package/dist/bundled.js +0 -123508
- package/dist/bundled.js.map +0 -828
- package/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { FeedbackRequest } from '@anvil/common/schemas/feedback'
|
|
4
|
+
|
|
5
|
+
import { createAnvilApiClient } from '../anvil-api'
|
|
6
|
+
|
|
7
|
+
// Type for mocked fetch function
|
|
8
|
+
type MockFetch = (url: string, options?: RequestInit) => Promise<Response>
|
|
9
|
+
|
|
10
|
+
describe('createAnvilApiClient', () => {
|
|
11
|
+
let mockFetch: ReturnType<typeof mock<MockFetch>>
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockFetch = mock<MockFetch>(() =>
|
|
15
|
+
Promise.resolve({
|
|
16
|
+
ok: true,
|
|
17
|
+
status: 200,
|
|
18
|
+
json: () => Promise.resolve({ id: 'test-id' }),
|
|
19
|
+
} as Response),
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('client creation', () => {
|
|
24
|
+
test('should create client with default base URL', () => {
|
|
25
|
+
const client = createAnvilApiClient()
|
|
26
|
+
expect(client.baseUrl).toBeTruthy()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('should create client with custom base URL', () => {
|
|
30
|
+
const client = createAnvilApiClient({ baseUrl: 'https://custom.api' })
|
|
31
|
+
expect(client.baseUrl).toBe('https://custom.api')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should store auth token', () => {
|
|
35
|
+
const client = createAnvilApiClient({ authToken: 'test-token' })
|
|
36
|
+
expect(client.authToken).toBe('test-token')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('GET requests', () => {
|
|
41
|
+
test('should make GET request with correct URL', async () => {
|
|
42
|
+
const client = createAnvilApiClient({
|
|
43
|
+
baseUrl: 'https://test.api',
|
|
44
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
await client.get('/api/v1/test', { retry: false })
|
|
48
|
+
|
|
49
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
50
|
+
const [url] = mockFetch.mock.calls[0] as [string, RequestInit | undefined]
|
|
51
|
+
expect(url).toBe('https://test.api/api/v1/test')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('should add query parameters', async () => {
|
|
55
|
+
const client = createAnvilApiClient({
|
|
56
|
+
baseUrl: 'https://test.api',
|
|
57
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
await client.get('/api/v1/me', {
|
|
61
|
+
query: { fields: 'id,email' },
|
|
62
|
+
retry: false,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const [url] = mockFetch.mock.calls[0] as [string, RequestInit | undefined]
|
|
66
|
+
expect(url).toBe('https://test.api/api/v1/me?fields=id%2Cemail')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('should include Authorization header when authToken provided', async () => {
|
|
70
|
+
const client = createAnvilApiClient({
|
|
71
|
+
baseUrl: 'https://test.api',
|
|
72
|
+
authToken: 'my-token',
|
|
73
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
await client.get('/api/v1/test', { retry: false })
|
|
77
|
+
|
|
78
|
+
const [, options] = mockFetch.mock.calls[0] as [
|
|
79
|
+
string,
|
|
80
|
+
RequestInit | undefined,
|
|
81
|
+
]
|
|
82
|
+
expect(options?.headers).toEqual({
|
|
83
|
+
Authorization: 'Bearer my-token',
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('should not include Authorization header when includeAuth is false', async () => {
|
|
88
|
+
const client = createAnvilApiClient({
|
|
89
|
+
baseUrl: 'https://test.api',
|
|
90
|
+
authToken: 'my-token',
|
|
91
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await client.get('/api/v1/test', { includeAuth: false, retry: false })
|
|
95
|
+
|
|
96
|
+
const [, options] = mockFetch.mock.calls[0] as [
|
|
97
|
+
string,
|
|
98
|
+
RequestInit | undefined,
|
|
99
|
+
]
|
|
100
|
+
expect(options?.headers).toEqual({})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('POST requests', () => {
|
|
105
|
+
test('should make POST request with JSON body', async () => {
|
|
106
|
+
const client = createAnvilApiClient({
|
|
107
|
+
baseUrl: 'https://test.api',
|
|
108
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await client.post('/api/v1/test', { key: 'value' }, { retry: false })
|
|
112
|
+
|
|
113
|
+
const [, options] = mockFetch.mock.calls[0] as [
|
|
114
|
+
string,
|
|
115
|
+
RequestInit | undefined,
|
|
116
|
+
]
|
|
117
|
+
expect(options?.method).toBe('POST')
|
|
118
|
+
expect(options?.headers).toEqual({
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
})
|
|
121
|
+
expect(options?.body).toBe('{"key":"value"}')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('should include Cookie header when includeCookie is true', async () => {
|
|
125
|
+
const client = createAnvilApiClient({
|
|
126
|
+
baseUrl: 'https://test.api',
|
|
127
|
+
authToken: 'my-token',
|
|
128
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await client.post(
|
|
132
|
+
'/api/v1/test',
|
|
133
|
+
{ data: 'test' },
|
|
134
|
+
{ includeCookie: true, includeAuth: false, retry: false },
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const [, options] = mockFetch.mock.calls[0] as [
|
|
138
|
+
string,
|
|
139
|
+
RequestInit | undefined,
|
|
140
|
+
]
|
|
141
|
+
expect(options?.headers).toEqual({
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
Cookie: 'next-auth.session-token=my-token;',
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('PUT requests', () => {
|
|
149
|
+
test('should make PUT request with JSON body', async () => {
|
|
150
|
+
const client = createAnvilApiClient({
|
|
151
|
+
baseUrl: 'https://test.api',
|
|
152
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await client.put('/api/v1/test', { key: 'value' }, { retry: false })
|
|
156
|
+
|
|
157
|
+
const [, options] = mockFetch.mock.calls[0] as [
|
|
158
|
+
string,
|
|
159
|
+
RequestInit | undefined,
|
|
160
|
+
]
|
|
161
|
+
expect(options?.method).toBe('PUT')
|
|
162
|
+
expect(options?.headers).toEqual({
|
|
163
|
+
'Content-Type': 'application/json',
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('PATCH requests', () => {
|
|
169
|
+
test('should make PATCH request with JSON body', async () => {
|
|
170
|
+
const client = createAnvilApiClient({
|
|
171
|
+
baseUrl: 'https://test.api',
|
|
172
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await client.patch('/api/v1/test', { key: 'value' }, { retry: false })
|
|
176
|
+
|
|
177
|
+
const [, options] = mockFetch.mock.calls[0] as [
|
|
178
|
+
string,
|
|
179
|
+
RequestInit | undefined,
|
|
180
|
+
]
|
|
181
|
+
expect(options?.method).toBe('PATCH')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('DELETE requests', () => {
|
|
186
|
+
test('should make DELETE request without body', async () => {
|
|
187
|
+
const client = createAnvilApiClient({
|
|
188
|
+
baseUrl: 'https://test.api',
|
|
189
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
await client.delete('/api/v1/test/123', { retry: false })
|
|
193
|
+
|
|
194
|
+
const [url, options] = mockFetch.mock.calls[0] as [
|
|
195
|
+
string,
|
|
196
|
+
RequestInit | undefined,
|
|
197
|
+
]
|
|
198
|
+
expect(url).toBe('https://test.api/api/v1/test/123')
|
|
199
|
+
expect(options?.method).toBe('DELETE')
|
|
200
|
+
expect(options?.body).toBeUndefined()
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('response handling', () => {
|
|
205
|
+
test('should return ok response with data', async () => {
|
|
206
|
+
const responseData = { id: 'user-123', email: 'test@example.com' }
|
|
207
|
+
const mockSuccessFetch = mock<MockFetch>(() =>
|
|
208
|
+
Promise.resolve({
|
|
209
|
+
ok: true,
|
|
210
|
+
status: 200,
|
|
211
|
+
json: () => Promise.resolve(responseData),
|
|
212
|
+
} as Response),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
const client = createAnvilApiClient({
|
|
216
|
+
baseUrl: 'https://test.api',
|
|
217
|
+
fetch: mockSuccessFetch as unknown as typeof fetch,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const result = await client.get('/api/v1/me', { retry: false })
|
|
221
|
+
|
|
222
|
+
expect(result.ok).toBe(true)
|
|
223
|
+
expect(result.status).toBe(200)
|
|
224
|
+
if (result.ok) {
|
|
225
|
+
expect(result.data).toEqual(responseData)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('should return error response with message', async () => {
|
|
230
|
+
const mockErrorFetch = mock<MockFetch>(() =>
|
|
231
|
+
Promise.resolve({
|
|
232
|
+
ok: false,
|
|
233
|
+
status: 401,
|
|
234
|
+
statusText: 'Unauthorized',
|
|
235
|
+
json: () => Promise.resolve({ error: 'Invalid token' }),
|
|
236
|
+
} as Response),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const client = createAnvilApiClient({
|
|
240
|
+
baseUrl: 'https://test.api',
|
|
241
|
+
fetch: mockErrorFetch as unknown as typeof fetch,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const result = await client.get('/api/v1/me', { retry: false })
|
|
245
|
+
|
|
246
|
+
expect(result.ok).toBe(false)
|
|
247
|
+
expect(result.status).toBe(401)
|
|
248
|
+
if (!result.ok) {
|
|
249
|
+
expect(result.error).toBe('Invalid token')
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('should handle non-JSON error responses', async () => {
|
|
254
|
+
const mockErrorFetch = mock<MockFetch>(() =>
|
|
255
|
+
Promise.resolve({
|
|
256
|
+
ok: false,
|
|
257
|
+
status: 500,
|
|
258
|
+
statusText: 'Internal Server Error',
|
|
259
|
+
json: () => Promise.reject(new Error('Not JSON')),
|
|
260
|
+
text: () => Promise.resolve('Server error occurred'),
|
|
261
|
+
} as unknown as Response),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const client = createAnvilApiClient({
|
|
265
|
+
baseUrl: 'https://test.api',
|
|
266
|
+
fetch: mockErrorFetch as unknown as typeof fetch,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const result = await client.get('/api/v1/test', { retry: false })
|
|
270
|
+
|
|
271
|
+
expect(result.ok).toBe(false)
|
|
272
|
+
expect(result.status).toBe(500)
|
|
273
|
+
if (!result.ok) {
|
|
274
|
+
expect(result.error).toBe('Server error occurred')
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('should handle 204 No Content responses', async () => {
|
|
279
|
+
const mockNoContentFetch = mock<MockFetch>(() =>
|
|
280
|
+
Promise.resolve({
|
|
281
|
+
ok: true,
|
|
282
|
+
status: 204,
|
|
283
|
+
json: () => Promise.reject(new Error('No content')),
|
|
284
|
+
} as Response),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
const client = createAnvilApiClient({
|
|
288
|
+
baseUrl: 'https://test.api',
|
|
289
|
+
fetch: mockNoContentFetch as unknown as typeof fetch,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const result = await client.delete('/api/v1/test/123', { retry: false })
|
|
293
|
+
|
|
294
|
+
expect(result.ok).toBe(true)
|
|
295
|
+
expect(result.status).toBe(204)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('retry logic', () => {
|
|
300
|
+
test('should retry on 500 errors', async () => {
|
|
301
|
+
let callCount = 0
|
|
302
|
+
const mockRetryFetch = mock<MockFetch>(() => {
|
|
303
|
+
callCount++
|
|
304
|
+
if (callCount < 3) {
|
|
305
|
+
return Promise.resolve({
|
|
306
|
+
ok: false,
|
|
307
|
+
status: 500,
|
|
308
|
+
statusText: 'Internal Server Error',
|
|
309
|
+
json: () => Promise.resolve({ error: 'Server error' }),
|
|
310
|
+
} as Response)
|
|
311
|
+
}
|
|
312
|
+
return Promise.resolve({
|
|
313
|
+
ok: true,
|
|
314
|
+
status: 200,
|
|
315
|
+
json: () => Promise.resolve({ success: true }),
|
|
316
|
+
} as Response)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const client = createAnvilApiClient({
|
|
320
|
+
baseUrl: 'https://test.api',
|
|
321
|
+
fetch: mockRetryFetch as unknown as typeof fetch,
|
|
322
|
+
retry: {
|
|
323
|
+
maxRetries: 3,
|
|
324
|
+
initialDelayMs: 10, // Fast for testing
|
|
325
|
+
maxDelayMs: 50,
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
const result = await client.get('/api/v1/test')
|
|
330
|
+
|
|
331
|
+
expect(result.ok).toBe(true)
|
|
332
|
+
expect(mockRetryFetch).toHaveBeenCalledTimes(3)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('should not retry on 400 errors', async () => {
|
|
336
|
+
const mockBadRequestFetch = mock<MockFetch>(() =>
|
|
337
|
+
Promise.resolve({
|
|
338
|
+
ok: false,
|
|
339
|
+
status: 400,
|
|
340
|
+
statusText: 'Bad Request',
|
|
341
|
+
json: () => Promise.resolve({ error: 'Invalid input' }),
|
|
342
|
+
} as Response),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
const client = createAnvilApiClient({
|
|
346
|
+
baseUrl: 'https://test.api',
|
|
347
|
+
fetch: mockBadRequestFetch as unknown as typeof fetch,
|
|
348
|
+
retry: { maxRetries: 3, initialDelayMs: 10 },
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
const result = await client.get('/api/v1/test')
|
|
352
|
+
|
|
353
|
+
expect(result.ok).toBe(false)
|
|
354
|
+
expect(result.status).toBe(400)
|
|
355
|
+
expect(mockBadRequestFetch).toHaveBeenCalledTimes(1)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
test('should respect retry: false option', async () => {
|
|
359
|
+
const mockServerErrorFetch = mock<MockFetch>(() =>
|
|
360
|
+
Promise.resolve({
|
|
361
|
+
ok: false,
|
|
362
|
+
status: 500,
|
|
363
|
+
statusText: 'Internal Server Error',
|
|
364
|
+
json: () => Promise.resolve({ error: 'Server error' }),
|
|
365
|
+
} as Response),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const client = createAnvilApiClient({
|
|
369
|
+
baseUrl: 'https://test.api',
|
|
370
|
+
fetch: mockServerErrorFetch as unknown as typeof fetch,
|
|
371
|
+
retry: { maxRetries: 3 },
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
const result = await client.get('/api/v1/test', { retry: false })
|
|
375
|
+
|
|
376
|
+
expect(result.ok).toBe(false)
|
|
377
|
+
expect(mockServerErrorFetch).toHaveBeenCalledTimes(1)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
test('should retry on network errors', async () => {
|
|
381
|
+
let callCount = 0
|
|
382
|
+
const mockNetworkErrorFetch = mock<MockFetch>(() => {
|
|
383
|
+
callCount++
|
|
384
|
+
if (callCount < 2) {
|
|
385
|
+
return Promise.reject(new Error('Network error: fetch failed'))
|
|
386
|
+
}
|
|
387
|
+
return Promise.resolve({
|
|
388
|
+
ok: true,
|
|
389
|
+
status: 200,
|
|
390
|
+
json: () => Promise.resolve({ success: true }),
|
|
391
|
+
} as Response)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
const client = createAnvilApiClient({
|
|
395
|
+
baseUrl: 'https://test.api',
|
|
396
|
+
fetch: mockNetworkErrorFetch as unknown as typeof fetch,
|
|
397
|
+
retry: { maxRetries: 3, initialDelayMs: 10 },
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const result = await client.get('/api/v1/test')
|
|
401
|
+
|
|
402
|
+
expect(result.ok).toBe(true)
|
|
403
|
+
expect(mockNetworkErrorFetch).toHaveBeenCalledTimes(2)
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('timeout', () => {
|
|
408
|
+
test('should pass abort signal to fetch', async () => {
|
|
409
|
+
let receivedSignal: AbortSignal | null | undefined
|
|
410
|
+
|
|
411
|
+
const mockFetchWithSignal = mock<MockFetch>(
|
|
412
|
+
async (_url: string, options?: RequestInit) => {
|
|
413
|
+
receivedSignal = options?.signal
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
status: 200,
|
|
417
|
+
json: () => Promise.resolve({ success: true }),
|
|
418
|
+
} as Response
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
const client = createAnvilApiClient({
|
|
423
|
+
baseUrl: 'https://test.api',
|
|
424
|
+
fetch: mockFetchWithSignal as unknown as typeof fetch,
|
|
425
|
+
defaultTimeoutMs: 5000,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
await client.get('/api/v1/test', { retry: false })
|
|
429
|
+
|
|
430
|
+
expect(receivedSignal).toBeDefined()
|
|
431
|
+
expect(receivedSignal instanceof AbortSignal).toBe(true)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
test('should handle abort error from fetch', async () => {
|
|
435
|
+
const mockAbortFetch = mock<MockFetch>(() => {
|
|
436
|
+
const error = new Error('The operation was aborted')
|
|
437
|
+
error.name = 'AbortError'
|
|
438
|
+
return Promise.reject(error)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const client = createAnvilApiClient({
|
|
442
|
+
baseUrl: 'https://test.api',
|
|
443
|
+
fetch: mockAbortFetch as unknown as typeof fetch,
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
// Should retry on abort errors
|
|
447
|
+
await expect(
|
|
448
|
+
client.get('/api/v1/test', { retry: false }),
|
|
449
|
+
).rejects.toThrow('The operation was aborted')
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe('custom headers', () => {
|
|
454
|
+
test('should merge custom headers', async () => {
|
|
455
|
+
const client = createAnvilApiClient({
|
|
456
|
+
baseUrl: 'https://test.api',
|
|
457
|
+
authToken: 'my-token',
|
|
458
|
+
fetch: mockFetch as unknown as typeof fetch,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
await client.get('/api/v1/test', {
|
|
462
|
+
headers: { 'X-Custom-Header': 'custom-value' },
|
|
463
|
+
retry: false,
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
const [, options] = mockFetch.mock.calls[0] as [
|
|
467
|
+
string,
|
|
468
|
+
RequestInit | undefined,
|
|
469
|
+
]
|
|
470
|
+
expect(options?.headers).toEqual({
|
|
471
|
+
'X-Custom-Header': 'custom-value',
|
|
472
|
+
Authorization: 'Bearer my-token',
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
describe('network error formatting', () => {
|
|
478
|
+
test('formats TLS certificate errors with proxy and trust-store guidance', async () => {
|
|
479
|
+
const mockTlsFetch = mock<MockFetch>(() => {
|
|
480
|
+
const error = new Error('self signed certificate in certificate chain')
|
|
481
|
+
return Promise.reject(error)
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const client = createAnvilApiClient({
|
|
485
|
+
baseUrl: 'https://anvil.com',
|
|
486
|
+
fetch: mockTlsFetch as unknown as typeof fetch,
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
await expect(
|
|
490
|
+
client.post('/api/auth/cli/code', { fingerprintId: 'test' }),
|
|
491
|
+
).rejects.toThrow(
|
|
492
|
+
'TLS certificate verification failed for https://anvil.com.',
|
|
493
|
+
)
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
test('formats nested TLS certificate causes from fetch failures without retrying', async () => {
|
|
497
|
+
const mockTlsFetch = mock<MockFetch>(() => {
|
|
498
|
+
const cause = new Error('self signed certificate in certificate chain')
|
|
499
|
+
const error = new Error('fetch failed', { cause })
|
|
500
|
+
return Promise.reject(error)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const client = createAnvilApiClient({
|
|
504
|
+
baseUrl: 'https://anvil.com',
|
|
505
|
+
fetch: mockTlsFetch as unknown as typeof fetch,
|
|
506
|
+
retry: {
|
|
507
|
+
maxRetries: 3,
|
|
508
|
+
initialDelayMs: 10,
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
await expect(
|
|
513
|
+
client.post('/api/auth/cli/code', { fingerprintId: 'test' }),
|
|
514
|
+
).rejects.toThrow(
|
|
515
|
+
'TLS certificate verification failed for https://anvil.com.',
|
|
516
|
+
)
|
|
517
|
+
expect(mockTlsFetch).toHaveBeenCalledTimes(1)
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
describe('feedback method', () => {
|
|
522
|
+
const minimalFeedbackPayload: FeedbackRequest = {
|
|
523
|
+
category: 'other',
|
|
524
|
+
type: 'general',
|
|
525
|
+
text: 'test feedback',
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
test('should not retry on 429 (rate limit) responses', async () => {
|
|
529
|
+
const mockRateLimitFetch = mock<MockFetch>(() =>
|
|
530
|
+
Promise.resolve({
|
|
531
|
+
ok: false,
|
|
532
|
+
status: 429,
|
|
533
|
+
statusText: 'Too Many Requests',
|
|
534
|
+
json: () => Promise.resolve({ error: 'Rate limited' }),
|
|
535
|
+
} as Response),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
const client = createAnvilApiClient({
|
|
539
|
+
baseUrl: 'https://test.api',
|
|
540
|
+
fetch: mockRateLimitFetch as unknown as typeof fetch,
|
|
541
|
+
retry: { maxRetries: 3, initialDelayMs: 10 },
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
const result = await client.feedback(minimalFeedbackPayload)
|
|
545
|
+
|
|
546
|
+
expect(result.ok).toBe(false)
|
|
547
|
+
expect(result.status).toBe(429)
|
|
548
|
+
expect(mockRateLimitFetch).toHaveBeenCalledTimes(1)
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test('should not retry on 500 responses (non-idempotent endpoint)', async () => {
|
|
552
|
+
const mockServerErrorFetch = mock<MockFetch>(() =>
|
|
553
|
+
Promise.resolve({
|
|
554
|
+
ok: false,
|
|
555
|
+
status: 500,
|
|
556
|
+
statusText: 'Internal Server Error',
|
|
557
|
+
json: () => Promise.resolve({ error: 'Server error' }),
|
|
558
|
+
} as Response),
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
const client = createAnvilApiClient({
|
|
562
|
+
baseUrl: 'https://test.api',
|
|
563
|
+
fetch: mockServerErrorFetch as unknown as typeof fetch,
|
|
564
|
+
retry: {
|
|
565
|
+
maxRetries: 3,
|
|
566
|
+
initialDelayMs: 10,
|
|
567
|
+
maxDelayMs: 50,
|
|
568
|
+
},
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
const result = await client.feedback(minimalFeedbackPayload)
|
|
572
|
+
|
|
573
|
+
expect(result.ok).toBe(false)
|
|
574
|
+
expect(result.status).toBe(500)
|
|
575
|
+
expect(mockServerErrorFetch).toHaveBeenCalledTimes(1)
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
6
|
+
|
|
7
|
+
import { ensureCliTestEnv } from '../../__tests__/test-utils'
|
|
8
|
+
|
|
9
|
+
const OWNER_FILE = 'anvil-instance-owner.json'
|
|
10
|
+
|
|
11
|
+
ensureCliTestEnv()
|
|
12
|
+
|
|
13
|
+
const { getConfigDir } = await import('../auth')
|
|
14
|
+
const {
|
|
15
|
+
isAnvilInstanceOwnedByDeadLocalProcess,
|
|
16
|
+
recordAnvilInstanceOwner,
|
|
17
|
+
} = await import('../anvil-instance-owner')
|
|
18
|
+
|
|
19
|
+
describe('anvil instance owner', () => {
|
|
20
|
+
let originalHome: string | undefined
|
|
21
|
+
let tempHome: string
|
|
22
|
+
|
|
23
|
+
const ownerPath = () => path.join(getConfigDir(), OWNER_FILE)
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
originalHome = process.env.HOME
|
|
27
|
+
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'anvil-owner-'))
|
|
28
|
+
process.env.HOME = tempHome
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (originalHome === undefined) {
|
|
33
|
+
delete process.env.HOME
|
|
34
|
+
} else {
|
|
35
|
+
process.env.HOME = originalHome
|
|
36
|
+
}
|
|
37
|
+
fs.rmSync(tempHome, { recursive: true, force: true })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('does not classify the current process as dead', () => {
|
|
41
|
+
recordAnvilInstanceOwner('inst-current')
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
isAnvilInstanceOwnedByDeadLocalProcess('inst-current'),
|
|
45
|
+
).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('classifies a matching owner with a dead pid as dead', () => {
|
|
49
|
+
fs.mkdirSync(getConfigDir(), { recursive: true })
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
ownerPath(),
|
|
52
|
+
JSON.stringify({ instanceId: 'inst-dead', pid: 2_147_483_647 }),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(isAnvilInstanceOwnedByDeadLocalProcess('inst-dead')).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('ignores a dead pid for a different instance id', () => {
|
|
59
|
+
fs.mkdirSync(getConfigDir(), { recursive: true })
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
ownerPath(),
|
|
62
|
+
JSON.stringify({ instanceId: 'inst-other', pid: 2_147_483_647 }),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect(
|
|
66
|
+
isAnvilInstanceOwnedByDeadLocalProcess('inst-current'),
|
|
67
|
+
).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
})
|