@vellumai/assistant 0.6.3 → 0.6.4
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/ARCHITECTURE.md +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -65,11 +65,18 @@ const createCdpInspectClientMock = mock(
|
|
|
65
65
|
);
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Mutable config state. Tests flip `cdpInspectEnabled`
|
|
69
|
-
* the factory's config-based selection
|
|
70
|
-
* file.
|
|
68
|
+
* Mutable config state. Tests flip `cdpInspectEnabled` and
|
|
69
|
+
* `desktopAutoConfig` to control the factory's config-based selection
|
|
70
|
+
* without needing a real config file.
|
|
71
71
|
*/
|
|
72
72
|
let cdpInspectEnabled = false;
|
|
73
|
+
let desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Captured log calls for verifying fallback log payloads.
|
|
77
|
+
*/
|
|
78
|
+
const logWarnCalls: Array<{ args: unknown[] }> = [];
|
|
79
|
+
const logDebugCalls: Array<{ args: unknown[] }> = [];
|
|
73
80
|
|
|
74
81
|
mock.module("../extension-cdp-client.js", () => ({
|
|
75
82
|
createExtensionCdpClient: createExtensionCdpClientMock,
|
|
@@ -88,14 +95,36 @@ mock.module("../../../../config/loader.js", () => ({
|
|
|
88
95
|
host: "localhost",
|
|
89
96
|
port: 9222,
|
|
90
97
|
probeTimeoutMs: 500,
|
|
98
|
+
desktopAuto: desktopAutoConfig,
|
|
91
99
|
},
|
|
92
100
|
},
|
|
93
101
|
}),
|
|
94
102
|
}));
|
|
103
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
104
|
+
getLogger: () => ({
|
|
105
|
+
debug: (...args: unknown[]) => {
|
|
106
|
+
logDebugCalls.push({ args });
|
|
107
|
+
},
|
|
108
|
+
warn: (...args: unknown[]) => {
|
|
109
|
+
logWarnCalls.push({ args });
|
|
110
|
+
},
|
|
111
|
+
info: () => {},
|
|
112
|
+
error: () => {},
|
|
113
|
+
}),
|
|
114
|
+
}));
|
|
95
115
|
|
|
96
116
|
// Import under test AFTER mock.module calls so that the factory's
|
|
97
117
|
// top-level imports resolve to our fakes.
|
|
98
|
-
const {
|
|
118
|
+
const {
|
|
119
|
+
getCdpClient,
|
|
120
|
+
buildCandidateList,
|
|
121
|
+
buildChainedClient,
|
|
122
|
+
buildPinnedCandidateList,
|
|
123
|
+
_resetDesktopAutoCooldown,
|
|
124
|
+
_getDesktopAutoCooldownSince,
|
|
125
|
+
recordDesktopAutoCooldown,
|
|
126
|
+
isDesktopAutoCooldownActive,
|
|
127
|
+
} = await import("../factory.js");
|
|
99
128
|
|
|
100
129
|
/**
|
|
101
130
|
* Minimal ToolContext suitable for factory tests. Only the fields the
|
|
@@ -108,6 +137,27 @@ function makeContext(
|
|
|
108
137
|
return overrides as unknown as ToolContext;
|
|
109
138
|
}
|
|
110
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Create a fake HostBrowserProxy that reports as available.
|
|
142
|
+
*/
|
|
143
|
+
function makeAvailableProxy(): HostBrowserProxy {
|
|
144
|
+
return {
|
|
145
|
+
request: mock(async () => ({})),
|
|
146
|
+
isAvailable: () => true,
|
|
147
|
+
} as unknown as HostBrowserProxy;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a fake HostBrowserProxy that reports as unavailable
|
|
152
|
+
* (proxy exists but client is disconnected).
|
|
153
|
+
*/
|
|
154
|
+
function makeUnavailableProxy(): HostBrowserProxy {
|
|
155
|
+
return {
|
|
156
|
+
request: mock(async () => ({})),
|
|
157
|
+
isAvailable: () => false,
|
|
158
|
+
} as unknown as HostBrowserProxy;
|
|
159
|
+
}
|
|
160
|
+
|
|
111
161
|
describe("getCdpClient", () => {
|
|
112
162
|
beforeEach(() => {
|
|
113
163
|
createExtensionCdpClientMock.mockClear();
|
|
@@ -117,12 +167,16 @@ describe("getCdpClient", () => {
|
|
|
117
167
|
lastLocalClient = undefined;
|
|
118
168
|
lastCdpInspectClient = undefined;
|
|
119
169
|
cdpInspectEnabled = false;
|
|
170
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
171
|
+
_resetDesktopAutoCooldown();
|
|
172
|
+
logWarnCalls.length = 0;
|
|
173
|
+
logDebugCalls.length = 0;
|
|
120
174
|
});
|
|
121
175
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
176
|
+
// ── Candidate selection (kind reported before first send) ────────────
|
|
177
|
+
|
|
178
|
+
test("routes to ExtensionCdpClient when hostBrowserProxy is set and available", async () => {
|
|
179
|
+
const fakeProxy = makeAvailableProxy();
|
|
126
180
|
const ctx = makeContext({
|
|
127
181
|
conversationId: "test-convo",
|
|
128
182
|
hostBrowserProxy: fakeProxy,
|
|
@@ -130,8 +184,16 @@ describe("getCdpClient", () => {
|
|
|
130
184
|
|
|
131
185
|
const client = getCdpClient(ctx);
|
|
132
186
|
|
|
187
|
+
// kind should reflect extension before first send (top candidate)
|
|
133
188
|
expect(client.kind).toBe("extension");
|
|
134
189
|
expect(client.conversationId).toBe("test-convo");
|
|
190
|
+
|
|
191
|
+
// Lazy creation: client is not created until first send
|
|
192
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
193
|
+
"Page.navigate",
|
|
194
|
+
{ url: "https://example.com" },
|
|
195
|
+
);
|
|
196
|
+
expect(result).toEqual({ ok: true, via: "extension" });
|
|
135
197
|
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
136
198
|
expect(createExtensionCdpClientMock).toHaveBeenCalledWith(
|
|
137
199
|
fakeProxy,
|
|
@@ -141,11 +203,50 @@ describe("getCdpClient", () => {
|
|
|
141
203
|
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
142
204
|
});
|
|
143
205
|
|
|
144
|
-
test("extension
|
|
206
|
+
test("skips extension when hostBrowserProxy is present but unavailable", async () => {
|
|
207
|
+
const fakeProxy = makeUnavailableProxy();
|
|
208
|
+
const ctx = makeContext({
|
|
209
|
+
conversationId: "disconnected-proxy",
|
|
210
|
+
hostBrowserProxy: fakeProxy,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const client = getCdpClient(ctx);
|
|
214
|
+
|
|
215
|
+
// Should fall through to local since extension is not available
|
|
216
|
+
expect(client.kind).toBe("local");
|
|
217
|
+
expect(client.conversationId).toBe("disconnected-proxy");
|
|
218
|
+
|
|
219
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
220
|
+
"Page.navigate",
|
|
221
|
+
);
|
|
222
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
223
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
224
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("skips extension but uses cdp-inspect when proxy unavailable and cdp-inspect enabled", async () => {
|
|
228
|
+
cdpInspectEnabled = true;
|
|
229
|
+
const fakeProxy = makeUnavailableProxy();
|
|
230
|
+
const ctx = makeContext({
|
|
231
|
+
conversationId: "disconnected-inspect",
|
|
232
|
+
hostBrowserProxy: fakeProxy,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const client = getCdpClient(ctx);
|
|
236
|
+
|
|
237
|
+
expect(client.kind).toBe("cdp-inspect");
|
|
238
|
+
|
|
239
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
240
|
+
"Page.navigate",
|
|
241
|
+
);
|
|
242
|
+
expect(result).toEqual({ ok: true, via: "cdp-inspect" });
|
|
243
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
244
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("extension wins even when cdpInspect is enabled", async () => {
|
|
145
248
|
cdpInspectEnabled = true;
|
|
146
|
-
const fakeProxy =
|
|
147
|
-
request: mock(async () => ({})),
|
|
148
|
-
} as unknown as HostBrowserProxy;
|
|
249
|
+
const fakeProxy = makeAvailableProxy();
|
|
149
250
|
const ctx = makeContext({
|
|
150
251
|
conversationId: "ext-wins",
|
|
151
252
|
hostBrowserProxy: fakeProxy,
|
|
@@ -154,12 +255,16 @@ describe("getCdpClient", () => {
|
|
|
154
255
|
const client = getCdpClient(ctx);
|
|
155
256
|
|
|
156
257
|
expect(client.kind).toBe("extension");
|
|
258
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
259
|
+
"Page.navigate",
|
|
260
|
+
);
|
|
261
|
+
expect(result).toEqual({ ok: true, via: "extension" });
|
|
157
262
|
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
158
263
|
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
159
264
|
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
160
265
|
});
|
|
161
266
|
|
|
162
|
-
test("routes to CdpInspectClient when cdpInspect is enabled and extension is absent", () => {
|
|
267
|
+
test("routes to CdpInspectClient when cdpInspect is enabled and extension is absent", async () => {
|
|
163
268
|
cdpInspectEnabled = true;
|
|
164
269
|
const ctx = makeContext({
|
|
165
270
|
conversationId: "inspect-convo",
|
|
@@ -170,6 +275,12 @@ describe("getCdpClient", () => {
|
|
|
170
275
|
|
|
171
276
|
expect(client.kind).toBe("cdp-inspect");
|
|
172
277
|
expect(client.conversationId).toBe("inspect-convo");
|
|
278
|
+
|
|
279
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
280
|
+
"Page.navigate",
|
|
281
|
+
{ url: "https://example.com" },
|
|
282
|
+
);
|
|
283
|
+
expect(result).toEqual({ ok: true, via: "cdp-inspect" });
|
|
173
284
|
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
174
285
|
expect(createCdpInspectClientMock).toHaveBeenCalledWith("inspect-convo", {
|
|
175
286
|
host: "localhost",
|
|
@@ -180,7 +291,7 @@ describe("getCdpClient", () => {
|
|
|
180
291
|
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
181
292
|
});
|
|
182
293
|
|
|
183
|
-
test("routes to LocalCdpClient when cdpInspect is disabled and extension is absent", () => {
|
|
294
|
+
test("routes to LocalCdpClient when cdpInspect is disabled and extension is absent", async () => {
|
|
184
295
|
cdpInspectEnabled = false;
|
|
185
296
|
const ctx = makeContext({
|
|
186
297
|
conversationId: "local-convo",
|
|
@@ -191,29 +302,65 @@ describe("getCdpClient", () => {
|
|
|
191
302
|
|
|
192
303
|
expect(client.kind).toBe("local");
|
|
193
304
|
expect(client.conversationId).toBe("local-convo");
|
|
305
|
+
|
|
306
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
307
|
+
"Runtime.evaluate",
|
|
308
|
+
{ expression: "1+1" },
|
|
309
|
+
);
|
|
310
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
194
311
|
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
195
312
|
expect(createLocalCdpClientMock).toHaveBeenCalledWith("local-convo");
|
|
196
313
|
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
197
314
|
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
198
315
|
});
|
|
199
316
|
|
|
200
|
-
test("routes to LocalCdpClient when hostBrowserProxy key is omitted", () => {
|
|
317
|
+
test("routes to LocalCdpClient when hostBrowserProxy key is omitted", async () => {
|
|
201
318
|
const ctx = makeContext({ conversationId: "another-convo" });
|
|
202
319
|
|
|
203
320
|
const client = getCdpClient(ctx);
|
|
204
321
|
|
|
205
322
|
expect(client.kind).toBe("local");
|
|
206
323
|
expect(client.conversationId).toBe("another-convo");
|
|
324
|
+
|
|
325
|
+
await client.send("Runtime.evaluate");
|
|
207
326
|
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
208
327
|
expect(createLocalCdpClientMock).toHaveBeenCalledWith("another-convo");
|
|
209
328
|
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
210
329
|
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
211
330
|
});
|
|
212
331
|
|
|
332
|
+
// ── Backwards compatibility: omitted mode behaves as auto ───────────
|
|
333
|
+
|
|
334
|
+
test("getCdpClient without options behaves identically to auto mode", async () => {
|
|
335
|
+
const fakeProxy = makeAvailableProxy();
|
|
336
|
+
const ctx = makeContext({
|
|
337
|
+
conversationId: "no-opts",
|
|
338
|
+
hostBrowserProxy: fakeProxy,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const client = getCdpClient(ctx);
|
|
342
|
+
expect(client.kind).toBe("extension");
|
|
343
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
344
|
+
"Page.navigate",
|
|
345
|
+
);
|
|
346
|
+
expect(result).toEqual({ ok: true, via: "extension" });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("getCdpClient with explicit auto mode behaves identically to omitted mode", async () => {
|
|
350
|
+
const ctx = makeContext({ conversationId: "explicit-auto" });
|
|
351
|
+
|
|
352
|
+
const client = getCdpClient(ctx, { mode: "auto" });
|
|
353
|
+
expect(client.kind).toBe("local");
|
|
354
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
355
|
+
"Runtime.evaluate",
|
|
356
|
+
);
|
|
357
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ── send() forwarding ────────────────────────────────────────────────
|
|
361
|
+
|
|
213
362
|
test("forwards send() through the manager to the extension-backed client", async () => {
|
|
214
|
-
const fakeProxy =
|
|
215
|
-
request: mock(async () => ({})),
|
|
216
|
-
} as unknown as HostBrowserProxy;
|
|
363
|
+
const fakeProxy = makeAvailableProxy();
|
|
217
364
|
const ctx = makeContext({
|
|
218
365
|
conversationId: "send-ext",
|
|
219
366
|
hostBrowserProxy: fakeProxy,
|
|
@@ -277,25 +424,43 @@ describe("getCdpClient", () => {
|
|
|
277
424
|
expect(lastLocalClient).toBeUndefined();
|
|
278
425
|
});
|
|
279
426
|
|
|
280
|
-
|
|
281
|
-
|
|
427
|
+
// ── Error propagation ────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
test("propagates CdpError (cdp_error) thrown by the underlying client without failover", async () => {
|
|
430
|
+
cdpInspectEnabled = true;
|
|
431
|
+
const ctx = makeContext({ conversationId: "err-no-failover" });
|
|
282
432
|
const client = getCdpClient(ctx);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
433
|
+
|
|
434
|
+
// Override cdp-inspect client to throw a cdp_error
|
|
435
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
436
|
+
(conversationId: string) => {
|
|
437
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
438
|
+
c.send = mock(async () => {
|
|
439
|
+
throw new CdpError("cdp_error", "kaboom", {
|
|
440
|
+
cdpMethod: "Page.navigate",
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
lastCdpInspectClient = c;
|
|
444
|
+
return c;
|
|
445
|
+
},
|
|
446
|
+
);
|
|
289
447
|
|
|
290
448
|
await expect(
|
|
291
449
|
client.send("Page.navigate", { url: "https://example.com" }),
|
|
292
|
-
).rejects.
|
|
450
|
+
).rejects.toMatchObject({ code: "cdp_error", message: "kaboom" });
|
|
451
|
+
|
|
452
|
+
// Should NOT have fallen through to local
|
|
453
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
293
454
|
});
|
|
294
455
|
|
|
295
456
|
test("propagates caller AbortSignal to the underlying client", async () => {
|
|
296
457
|
const ctx = makeContext({ conversationId: "abort-local" });
|
|
297
458
|
const client = getCdpClient(ctx);
|
|
298
459
|
const controller = new AbortController();
|
|
460
|
+
|
|
461
|
+
// First, do a normal send to establish the sticky backend
|
|
462
|
+
await client.send("Runtime.evaluate", { expression: "1" });
|
|
463
|
+
|
|
299
464
|
let sawSignal: AbortSignal | undefined;
|
|
300
465
|
lastLocalClient!.send = mock(
|
|
301
466
|
async (
|
|
@@ -318,10 +483,16 @@ describe("getCdpClient", () => {
|
|
|
318
483
|
expect(sawSignal).toBe(controller.signal);
|
|
319
484
|
});
|
|
320
485
|
|
|
486
|
+
// ── Dispose ──────────────────────────────────────────────────────────
|
|
487
|
+
|
|
321
488
|
test("dispose() tears down the underlying client and rejects further sends", async () => {
|
|
322
489
|
const ctx = makeContext({ conversationId: "dispose-local" });
|
|
323
490
|
const client = getCdpClient(ctx);
|
|
324
491
|
|
|
492
|
+
// Trigger client creation via send
|
|
493
|
+
await client.send("Runtime.evaluate");
|
|
494
|
+
expect(lastLocalClient).toBeDefined();
|
|
495
|
+
|
|
325
496
|
client.dispose();
|
|
326
497
|
expect(lastLocalClient?.dispose).toHaveBeenCalledTimes(1);
|
|
327
498
|
|
|
@@ -334,26 +505,26 @@ describe("getCdpClient", () => {
|
|
|
334
505
|
});
|
|
335
506
|
});
|
|
336
507
|
|
|
337
|
-
test("dispose() on an extension-backed client tears down the extension client", () => {
|
|
338
|
-
const fakeProxy =
|
|
339
|
-
request: mock(async () => ({})),
|
|
340
|
-
} as unknown as HostBrowserProxy;
|
|
508
|
+
test("dispose() on an extension-backed client tears down the extension client", async () => {
|
|
509
|
+
const fakeProxy = makeAvailableProxy();
|
|
341
510
|
const ctx = makeContext({
|
|
342
511
|
conversationId: "dispose-ext",
|
|
343
512
|
hostBrowserProxy: fakeProxy,
|
|
344
513
|
});
|
|
345
514
|
|
|
346
515
|
const client = getCdpClient(ctx);
|
|
516
|
+
await client.send("Page.navigate");
|
|
347
517
|
client.dispose();
|
|
348
518
|
|
|
349
519
|
expect(lastExtensionClient?.dispose).toHaveBeenCalledTimes(1);
|
|
350
520
|
});
|
|
351
521
|
|
|
352
|
-
test("dispose() on a cdp-inspect-backed client tears down the inspect client", () => {
|
|
522
|
+
test("dispose() on a cdp-inspect-backed client tears down the inspect client", async () => {
|
|
353
523
|
cdpInspectEnabled = true;
|
|
354
524
|
const ctx = makeContext({ conversationId: "dispose-inspect" });
|
|
355
525
|
|
|
356
526
|
const client = getCdpClient(ctx);
|
|
527
|
+
await client.send("Page.navigate");
|
|
357
528
|
client.dispose();
|
|
358
529
|
|
|
359
530
|
expect(lastCdpInspectClient?.dispose).toHaveBeenCalledTimes(1);
|
|
@@ -364,6 +535,7 @@ describe("getCdpClient", () => {
|
|
|
364
535
|
const ctx = makeContext({ conversationId: "post-dispose-inspect" });
|
|
365
536
|
|
|
366
537
|
const client = getCdpClient(ctx);
|
|
538
|
+
await client.send("Page.navigate");
|
|
367
539
|
client.dispose();
|
|
368
540
|
|
|
369
541
|
// Double dispose is a no-op.
|
|
@@ -374,4 +546,1448 @@ describe("getCdpClient", () => {
|
|
|
374
546
|
code: "disposed",
|
|
375
547
|
});
|
|
376
548
|
});
|
|
549
|
+
|
|
550
|
+
test("dispose() before first send still rejects further sends", async () => {
|
|
551
|
+
const ctx = makeContext({ conversationId: "dispose-before-send" });
|
|
552
|
+
const client = getCdpClient(ctx);
|
|
553
|
+
|
|
554
|
+
client.dispose();
|
|
555
|
+
|
|
556
|
+
await expect(client.send("Runtime.evaluate")).rejects.toMatchObject({
|
|
557
|
+
code: "disposed",
|
|
558
|
+
});
|
|
559
|
+
// No clients should have been created
|
|
560
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ── transportInterface backwards compatibility ──────────────────────
|
|
564
|
+
|
|
565
|
+
test("context without transportInterface still routes to local backend", async () => {
|
|
566
|
+
const ctx = makeContext({ conversationId: "no-interface" });
|
|
567
|
+
expect(ctx.transportInterface).toBeUndefined();
|
|
568
|
+
|
|
569
|
+
const client = getCdpClient(ctx);
|
|
570
|
+
|
|
571
|
+
expect(client.kind).toBe("local");
|
|
572
|
+
expect(client.conversationId).toBe("no-interface");
|
|
573
|
+
await client.send("Runtime.evaluate");
|
|
574
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("context with transportInterface set routes normally to extension backend", async () => {
|
|
578
|
+
const fakeProxy = makeAvailableProxy();
|
|
579
|
+
const ctx = makeContext({
|
|
580
|
+
conversationId: "macos-ext",
|
|
581
|
+
hostBrowserProxy: fakeProxy,
|
|
582
|
+
transportInterface: "macos",
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const client = getCdpClient(ctx);
|
|
586
|
+
|
|
587
|
+
expect(client.kind).toBe("extension");
|
|
588
|
+
expect(client.conversationId).toBe("macos-ext");
|
|
589
|
+
await client.send("Page.navigate");
|
|
590
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("context with transportInterface=macos routes to desktop-auto cdp-inspect when no proxy", async () => {
|
|
594
|
+
const ctx = makeContext({
|
|
595
|
+
conversationId: "macos-local",
|
|
596
|
+
transportInterface: "macos",
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const client = getCdpClient(ctx);
|
|
600
|
+
|
|
601
|
+
// desktopAuto.enabled is true by default and no proxy is provisioned,
|
|
602
|
+
// so cdp-inspect is the first candidate (desktop-auto path).
|
|
603
|
+
expect(client.kind).toBe("cdp-inspect");
|
|
604
|
+
expect(client.conversationId).toBe("macos-local");
|
|
605
|
+
await client.send("Page.navigate");
|
|
606
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("context with transportInterface set routes to cdp-inspect when enabled", async () => {
|
|
610
|
+
cdpInspectEnabled = true;
|
|
611
|
+
const ctx = makeContext({
|
|
612
|
+
conversationId: "macos-inspect",
|
|
613
|
+
transportInterface: "macos",
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const client = getCdpClient(ctx);
|
|
617
|
+
|
|
618
|
+
expect(client.kind).toBe("cdp-inspect");
|
|
619
|
+
expect(client.conversationId).toBe("macos-inspect");
|
|
620
|
+
await client.send("Page.navigate");
|
|
621
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// ── buildCandidateList tests ─────────────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
describe("buildCandidateList", () => {
|
|
628
|
+
beforeEach(() => {
|
|
629
|
+
cdpInspectEnabled = false;
|
|
630
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
631
|
+
_resetDesktopAutoCooldown();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("includes extension candidate when proxy is present and available", () => {
|
|
635
|
+
const fakeProxy = makeAvailableProxy();
|
|
636
|
+
const ctx = makeContext({
|
|
637
|
+
conversationId: "candidates-ext",
|
|
638
|
+
hostBrowserProxy: fakeProxy,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const candidates = buildCandidateList(ctx);
|
|
642
|
+
|
|
643
|
+
expect(candidates.length).toBeGreaterThanOrEqual(2);
|
|
644
|
+
expect(candidates[0].kind).toBe("extension");
|
|
645
|
+
// Local is always present as fallback
|
|
646
|
+
expect(candidates[candidates.length - 1].kind).toBe("local");
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("excludes extension candidate when proxy is present but unavailable", () => {
|
|
650
|
+
const fakeProxy = makeUnavailableProxy();
|
|
651
|
+
const ctx = makeContext({
|
|
652
|
+
conversationId: "candidates-no-ext",
|
|
653
|
+
hostBrowserProxy: fakeProxy,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const candidates = buildCandidateList(ctx);
|
|
657
|
+
|
|
658
|
+
expect(candidates.every((c) => c.kind !== "extension")).toBe(true);
|
|
659
|
+
expect(candidates[0].kind).toBe("local");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("includes cdp-inspect candidate when enabled in config", () => {
|
|
663
|
+
cdpInspectEnabled = true;
|
|
664
|
+
const ctx = makeContext({ conversationId: "candidates-inspect" });
|
|
665
|
+
|
|
666
|
+
const candidates = buildCandidateList(ctx);
|
|
667
|
+
|
|
668
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
669
|
+
expect(candidates[1].kind).toBe("local");
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("candidate order: extension > cdp-inspect > local when all present", () => {
|
|
673
|
+
cdpInspectEnabled = true;
|
|
674
|
+
const fakeProxy = makeAvailableProxy();
|
|
675
|
+
const ctx = makeContext({
|
|
676
|
+
conversationId: "candidates-all",
|
|
677
|
+
hostBrowserProxy: fakeProxy,
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const candidates = buildCandidateList(ctx);
|
|
681
|
+
|
|
682
|
+
expect(candidates.length).toBe(3);
|
|
683
|
+
expect(candidates[0].kind).toBe("extension");
|
|
684
|
+
expect(candidates[1].kind).toBe("cdp-inspect");
|
|
685
|
+
expect(candidates[2].kind).toBe("local");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("local is always included as final candidate", () => {
|
|
689
|
+
const ctx = makeContext({ conversationId: "candidates-local-only" });
|
|
690
|
+
|
|
691
|
+
const candidates = buildCandidateList(ctx);
|
|
692
|
+
|
|
693
|
+
expect(candidates.length).toBe(1);
|
|
694
|
+
expect(candidates[0].kind).toBe("local");
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// ── buildChainedClient failover tests ────────────────────────────────────
|
|
699
|
+
|
|
700
|
+
describe("buildChainedClient failover", () => {
|
|
701
|
+
beforeEach(() => {
|
|
702
|
+
createExtensionCdpClientMock.mockClear();
|
|
703
|
+
createLocalCdpClientMock.mockClear();
|
|
704
|
+
createCdpInspectClientMock.mockClear();
|
|
705
|
+
lastExtensionClient = undefined;
|
|
706
|
+
lastLocalClient = undefined;
|
|
707
|
+
lastCdpInspectClient = undefined;
|
|
708
|
+
cdpInspectEnabled = false;
|
|
709
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
710
|
+
_resetDesktopAutoCooldown();
|
|
711
|
+
logWarnCalls.length = 0;
|
|
712
|
+
logDebugCalls.length = 0;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("fails over from extension to local on transport_error", async () => {
|
|
716
|
+
const fakeProxy = makeAvailableProxy();
|
|
717
|
+
|
|
718
|
+
// Make extension client fail with transport_error
|
|
719
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
720
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
721
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
722
|
+
c.send = mock(async () => {
|
|
723
|
+
throw new CdpError(
|
|
724
|
+
"transport_error",
|
|
725
|
+
"Extension WebSocket disconnected",
|
|
726
|
+
{
|
|
727
|
+
cdpMethod: "Page.navigate",
|
|
728
|
+
},
|
|
729
|
+
);
|
|
730
|
+
});
|
|
731
|
+
lastExtensionClient = c;
|
|
732
|
+
return c;
|
|
733
|
+
},
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const ctx = makeContext({
|
|
737
|
+
conversationId: "failover-ext-to-local",
|
|
738
|
+
hostBrowserProxy: fakeProxy,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const client = getCdpClient(ctx);
|
|
742
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
743
|
+
"Page.navigate",
|
|
744
|
+
{ url: "https://example.com" },
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
748
|
+
// Extension was tried first
|
|
749
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
750
|
+
// Then local was used as fallback
|
|
751
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("fails over from extension to cdp-inspect to local on transport errors", async () => {
|
|
755
|
+
cdpInspectEnabled = true;
|
|
756
|
+
const fakeProxy = makeAvailableProxy();
|
|
757
|
+
|
|
758
|
+
// Make extension fail with transport_error
|
|
759
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
760
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
761
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
762
|
+
c.send = mock(async () => {
|
|
763
|
+
throw new CdpError("transport_error", "Extension disconnected", {
|
|
764
|
+
cdpMethod: "Page.navigate",
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
lastExtensionClient = c;
|
|
768
|
+
return c;
|
|
769
|
+
},
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
// Make cdp-inspect also fail with transport_error
|
|
773
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
774
|
+
(conversationId: string) => {
|
|
775
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
776
|
+
c.send = mock(async () => {
|
|
777
|
+
throw new CdpError("transport_error", "Chrome not running", {
|
|
778
|
+
cdpMethod: "Page.navigate",
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
lastCdpInspectClient = c;
|
|
782
|
+
return c;
|
|
783
|
+
},
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const ctx = makeContext({
|
|
787
|
+
conversationId: "failover-chain",
|
|
788
|
+
hostBrowserProxy: fakeProxy,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
const client = getCdpClient(ctx);
|
|
792
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
793
|
+
"Page.navigate",
|
|
794
|
+
{ url: "https://example.com" },
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
798
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
799
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
800
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("does NOT fail over on cdp_error -- propagates immediately", async () => {
|
|
804
|
+
cdpInspectEnabled = true;
|
|
805
|
+
const fakeProxy = makeAvailableProxy();
|
|
806
|
+
|
|
807
|
+
// Make extension fail with cdp_error (not transport_error)
|
|
808
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
809
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
810
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
811
|
+
c.send = mock(async () => {
|
|
812
|
+
throw new CdpError("cdp_error", "Protocol error", {
|
|
813
|
+
cdpMethod: "Page.navigate",
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
lastExtensionClient = c;
|
|
817
|
+
return c;
|
|
818
|
+
},
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
const ctx = makeContext({
|
|
822
|
+
conversationId: "no-failover-cdp-error",
|
|
823
|
+
hostBrowserProxy: fakeProxy,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const client = getCdpClient(ctx);
|
|
827
|
+
|
|
828
|
+
await expect(
|
|
829
|
+
client.send("Page.navigate", { url: "https://example.com" }),
|
|
830
|
+
).rejects.toMatchObject({
|
|
831
|
+
code: "cdp_error",
|
|
832
|
+
message: "Protocol error",
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// cdp-inspect and local should NOT have been tried
|
|
836
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
837
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("transport_error on last candidate propagates the error", async () => {
|
|
841
|
+
// Only local is available (no extension, no cdp-inspect)
|
|
842
|
+
const ctx = makeContext({ conversationId: "last-candidate-fail" });
|
|
843
|
+
|
|
844
|
+
// Make local fail with transport_error
|
|
845
|
+
createLocalCdpClientMock.mockImplementationOnce(
|
|
846
|
+
(conversationId: string) => {
|
|
847
|
+
const c = makeFakeLocalClient(conversationId);
|
|
848
|
+
c.send = mock(async () => {
|
|
849
|
+
throw new CdpError("transport_error", "Playwright failed to launch", {
|
|
850
|
+
cdpMethod: "Page.navigate",
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
lastLocalClient = c;
|
|
854
|
+
return c;
|
|
855
|
+
},
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const client = getCdpClient(ctx);
|
|
859
|
+
|
|
860
|
+
await expect(client.send("Page.navigate")).rejects.toMatchObject({
|
|
861
|
+
code: "transport_error",
|
|
862
|
+
message: "Playwright failed to launch",
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// ── Sticky backend tests ─────────────────────────────────────────────
|
|
867
|
+
|
|
868
|
+
test("backend becomes sticky after first successful command", async () => {
|
|
869
|
+
cdpInspectEnabled = true;
|
|
870
|
+
const fakeProxy = makeAvailableProxy();
|
|
871
|
+
|
|
872
|
+
// Make extension fail on first call with transport_error
|
|
873
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
874
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
875
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
876
|
+
c.send = mock(async () => {
|
|
877
|
+
throw new CdpError("transport_error", "Extension disconnected", {
|
|
878
|
+
cdpMethod: "Page.navigate",
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
lastExtensionClient = c;
|
|
882
|
+
return c;
|
|
883
|
+
},
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const ctx = makeContext({
|
|
887
|
+
conversationId: "sticky-test",
|
|
888
|
+
hostBrowserProxy: fakeProxy,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const client = getCdpClient(ctx);
|
|
892
|
+
|
|
893
|
+
// First send fails over from extension to cdp-inspect
|
|
894
|
+
const result1 = await client.send<{ ok: boolean; via: string }>(
|
|
895
|
+
"Page.navigate",
|
|
896
|
+
{ url: "https://example.com" },
|
|
897
|
+
);
|
|
898
|
+
expect(result1).toEqual({ ok: true, via: "cdp-inspect" });
|
|
899
|
+
|
|
900
|
+
// Second send should reuse cdp-inspect without trying extension again
|
|
901
|
+
const result2 = await client.send<{ ok: boolean; via: string }>(
|
|
902
|
+
"Runtime.evaluate",
|
|
903
|
+
{ expression: "1+1" },
|
|
904
|
+
);
|
|
905
|
+
expect(result2).toEqual({ ok: true, via: "cdp-inspect" });
|
|
906
|
+
|
|
907
|
+
// Extension should only have been constructed once (during failover)
|
|
908
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
909
|
+
// cdp-inspect should only have been constructed once (sticky)
|
|
910
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
911
|
+
// Local should never have been constructed
|
|
912
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
913
|
+
|
|
914
|
+
// Verify the sticky client's send was called for both commands
|
|
915
|
+
// The first call is from failover, the second from sticky path
|
|
916
|
+
expect(lastCdpInspectClient?.send).toHaveBeenCalledTimes(2);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test("sticky backend does not change on subsequent transport errors", async () => {
|
|
920
|
+
const ctx = makeContext({ conversationId: "sticky-err" });
|
|
921
|
+
|
|
922
|
+
const client = getCdpClient(ctx);
|
|
923
|
+
|
|
924
|
+
// First send succeeds, establishing local as sticky
|
|
925
|
+
await client.send("Runtime.evaluate", { expression: "1" });
|
|
926
|
+
expect(client.kind).toBe("local");
|
|
927
|
+
|
|
928
|
+
// Now make local throw a transport error on second send
|
|
929
|
+
lastLocalClient!.send = mock(async () => {
|
|
930
|
+
throw new CdpError("transport_error", "Connection lost");
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// The error should propagate without failover since backend is sticky
|
|
934
|
+
await expect(client.send("Page.navigate")).rejects.toMatchObject({
|
|
935
|
+
code: "transport_error",
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// ── Edge cases ───────────────────────────────────────────────────────
|
|
940
|
+
|
|
941
|
+
test("buildChainedClient throws on empty candidate list", () => {
|
|
942
|
+
expect(() => buildChainedClient("test", [])).toThrow(
|
|
943
|
+
"CDP factory: no backend candidates available",
|
|
944
|
+
);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test("kind reflects the active backend after failover", async () => {
|
|
948
|
+
const fakeProxy = makeAvailableProxy();
|
|
949
|
+
|
|
950
|
+
// Make extension fail
|
|
951
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
952
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
953
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
954
|
+
c.send = mock(async () => {
|
|
955
|
+
throw new CdpError("transport_error", "disconnected");
|
|
956
|
+
});
|
|
957
|
+
lastExtensionClient = c;
|
|
958
|
+
return c;
|
|
959
|
+
},
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
const ctx = makeContext({
|
|
963
|
+
conversationId: "kind-after-failover",
|
|
964
|
+
hostBrowserProxy: fakeProxy,
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
const client = getCdpClient(ctx);
|
|
968
|
+
|
|
969
|
+
// Before first send, kind reflects the first candidate
|
|
970
|
+
expect(client.kind).toBe("extension");
|
|
971
|
+
|
|
972
|
+
// After failover, kind should reflect the local backend
|
|
973
|
+
await client.send("Page.navigate");
|
|
974
|
+
expect(client.kind).toBe("local");
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("dispose cleans up failed backends from failover chain", async () => {
|
|
978
|
+
const fakeProxy = makeAvailableProxy();
|
|
979
|
+
|
|
980
|
+
// Make extension fail
|
|
981
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
982
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
983
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
984
|
+
c.send = mock(async () => {
|
|
985
|
+
throw new CdpError("transport_error", "disconnected");
|
|
986
|
+
});
|
|
987
|
+
lastExtensionClient = c;
|
|
988
|
+
return c;
|
|
989
|
+
},
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
const ctx = makeContext({
|
|
993
|
+
conversationId: "dispose-failover",
|
|
994
|
+
hostBrowserProxy: fakeProxy,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const client = getCdpClient(ctx);
|
|
998
|
+
await client.send("Page.navigate");
|
|
999
|
+
|
|
1000
|
+
// Now dispose -- both the failed extension backend and the
|
|
1001
|
+
// successful local backend should be cleaned up.
|
|
1002
|
+
client.dispose();
|
|
1003
|
+
|
|
1004
|
+
// The extension client's dispose was already called during
|
|
1005
|
+
// failover (via manager.disposeAll()), and local's dispose should
|
|
1006
|
+
// be called now
|
|
1007
|
+
expect(lastLocalClient?.dispose).toHaveBeenCalled();
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// ── Desktop-auto cdp-inspect for macOS ──────────────────────────────────
|
|
1012
|
+
|
|
1013
|
+
describe("desktop-auto cdp-inspect (macOS)", () => {
|
|
1014
|
+
beforeEach(() => {
|
|
1015
|
+
createExtensionCdpClientMock.mockClear();
|
|
1016
|
+
createLocalCdpClientMock.mockClear();
|
|
1017
|
+
createCdpInspectClientMock.mockClear();
|
|
1018
|
+
lastExtensionClient = undefined;
|
|
1019
|
+
lastLocalClient = undefined;
|
|
1020
|
+
lastCdpInspectClient = undefined;
|
|
1021
|
+
cdpInspectEnabled = false;
|
|
1022
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1023
|
+
_resetDesktopAutoCooldown();
|
|
1024
|
+
logWarnCalls.length = 0;
|
|
1025
|
+
logDebugCalls.length = 0;
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// ── buildCandidateList with desktopAuto ─────────────────────────────
|
|
1029
|
+
|
|
1030
|
+
test("macOS turn includes cdp-inspect candidate even when enabled is false", () => {
|
|
1031
|
+
const ctx = makeContext({
|
|
1032
|
+
conversationId: "macos-auto",
|
|
1033
|
+
transportInterface: "macos",
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const candidates = buildCandidateList(ctx);
|
|
1037
|
+
|
|
1038
|
+
expect(candidates.length).toBe(2);
|
|
1039
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
1040
|
+
expect(candidates[0].reason).toContain("desktopAuto");
|
|
1041
|
+
expect(candidates[1].kind).toBe("local");
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
test("macOS turn with extension available: extension > cdp-inspect > local", () => {
|
|
1045
|
+
const fakeProxy = makeAvailableProxy();
|
|
1046
|
+
const ctx = makeContext({
|
|
1047
|
+
conversationId: "macos-all",
|
|
1048
|
+
hostBrowserProxy: fakeProxy,
|
|
1049
|
+
transportInterface: "macos",
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
const candidates = buildCandidateList(ctx);
|
|
1053
|
+
|
|
1054
|
+
expect(candidates.length).toBe(3);
|
|
1055
|
+
expect(candidates[0].kind).toBe("extension");
|
|
1056
|
+
expect(candidates[1].kind).toBe("cdp-inspect");
|
|
1057
|
+
expect(candidates[1].reason).toContain("desktopAuto");
|
|
1058
|
+
expect(candidates[2].kind).toBe("local");
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("macOS turn with proxy unavailable skips desktop-auto cdp-inspect (extension intent)", () => {
|
|
1062
|
+
const fakeProxy = makeUnavailableProxy();
|
|
1063
|
+
const ctx = makeContext({
|
|
1064
|
+
conversationId: "macos-proxy-unavailable-no-inspect",
|
|
1065
|
+
hostBrowserProxy: fakeProxy,
|
|
1066
|
+
transportInterface: "macos",
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const candidates = buildCandidateList(ctx);
|
|
1070
|
+
|
|
1071
|
+
// Should only include local -- cdp-inspect is suppressed because extension
|
|
1072
|
+
// transport is expected (proxy exists) but temporarily unavailable.
|
|
1073
|
+
expect(candidates.length).toBe(1);
|
|
1074
|
+
expect(candidates[0].kind).toBe("local");
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
test("macOS turn with no proxy still includes desktop-auto cdp-inspect", () => {
|
|
1078
|
+
const ctx = makeContext({
|
|
1079
|
+
conversationId: "macos-no-proxy-inspect-allowed",
|
|
1080
|
+
transportInterface: "macos",
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
const candidates = buildCandidateList(ctx);
|
|
1084
|
+
|
|
1085
|
+
// No proxy provisioned => cdp-inspect remains available as fallback
|
|
1086
|
+
expect(candidates.length).toBe(2);
|
|
1087
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
1088
|
+
expect(candidates[0].reason).toContain("desktopAuto");
|
|
1089
|
+
expect(candidates[1].kind).toBe("local");
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
test("macOS turn with extension available still includes cdp-inspect as fallback", () => {
|
|
1093
|
+
const fakeProxy = makeAvailableProxy();
|
|
1094
|
+
const ctx = makeContext({
|
|
1095
|
+
conversationId: "macos-ext-available-inspect-fallback",
|
|
1096
|
+
hostBrowserProxy: fakeProxy,
|
|
1097
|
+
transportInterface: "macos",
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const candidates = buildCandidateList(ctx);
|
|
1101
|
+
|
|
1102
|
+
// Extension is available => extension + cdp-inspect (desktop-auto) + local
|
|
1103
|
+
expect(candidates.length).toBe(3);
|
|
1104
|
+
expect(candidates[0].kind).toBe("extension");
|
|
1105
|
+
expect(candidates[1].kind).toBe("cdp-inspect");
|
|
1106
|
+
expect(candidates[1].reason).toContain("desktopAuto");
|
|
1107
|
+
expect(candidates[2].kind).toBe("local");
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
test("macOS turn does NOT include cdp-inspect when desktopAuto.enabled is false", () => {
|
|
1111
|
+
desktopAutoConfig = { enabled: false, cooldownMs: 30_000 };
|
|
1112
|
+
const ctx = makeContext({
|
|
1113
|
+
conversationId: "macos-no-auto",
|
|
1114
|
+
transportInterface: "macos",
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
const candidates = buildCandidateList(ctx);
|
|
1118
|
+
|
|
1119
|
+
expect(candidates.length).toBe(1);
|
|
1120
|
+
expect(candidates[0].kind).toBe("local");
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test("non-macOS turn does NOT include cdp-inspect when enabled is false", () => {
|
|
1124
|
+
const ctx = makeContext({
|
|
1125
|
+
conversationId: "cli-no-auto",
|
|
1126
|
+
transportInterface: "cli",
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
const candidates = buildCandidateList(ctx);
|
|
1130
|
+
|
|
1131
|
+
expect(candidates.length).toBe(1);
|
|
1132
|
+
expect(candidates[0].kind).toBe("local");
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
test("non-macOS turn without transportInterface does NOT include cdp-inspect", () => {
|
|
1136
|
+
const ctx = makeContext({
|
|
1137
|
+
conversationId: "no-interface-no-auto",
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
const candidates = buildCandidateList(ctx);
|
|
1141
|
+
|
|
1142
|
+
expect(candidates.length).toBe(1);
|
|
1143
|
+
expect(candidates[0].kind).toBe("local");
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
test("explicit cdpInspect.enabled takes precedence over desktopAuto on macOS", () => {
|
|
1147
|
+
cdpInspectEnabled = true;
|
|
1148
|
+
const ctx = makeContext({
|
|
1149
|
+
conversationId: "macos-explicit",
|
|
1150
|
+
transportInterface: "macos",
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
const candidates = buildCandidateList(ctx);
|
|
1154
|
+
|
|
1155
|
+
// Should include cdp-inspect via the explicit path, not desktopAuto
|
|
1156
|
+
expect(candidates.length).toBe(2);
|
|
1157
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
1158
|
+
expect(candidates[0].reason).toBe("cdpInspect enabled in config");
|
|
1159
|
+
expect(candidates[1].kind).toBe("local");
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// ── Cooldown behaviour ──────────────────────────────────────────────
|
|
1163
|
+
|
|
1164
|
+
test("macOS turn skips cdp-inspect when cooldown is active", () => {
|
|
1165
|
+
// Record a cooldown
|
|
1166
|
+
recordDesktopAutoCooldown();
|
|
1167
|
+
|
|
1168
|
+
const ctx = makeContext({
|
|
1169
|
+
conversationId: "macos-cooldown",
|
|
1170
|
+
transportInterface: "macos",
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
const candidates = buildCandidateList(ctx);
|
|
1174
|
+
|
|
1175
|
+
// Should skip cdp-inspect and only include local
|
|
1176
|
+
expect(candidates.length).toBe(1);
|
|
1177
|
+
expect(candidates[0].kind).toBe("local");
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test("macOS turn includes cdp-inspect after cooldown expires", () => {
|
|
1181
|
+
// Set cooldown to 0 (disabled)
|
|
1182
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 0 };
|
|
1183
|
+
|
|
1184
|
+
// Record a "cooldown" -- but with cooldownMs=0 it should be ignored
|
|
1185
|
+
recordDesktopAutoCooldown();
|
|
1186
|
+
|
|
1187
|
+
const ctx = makeContext({
|
|
1188
|
+
conversationId: "macos-expired-cooldown",
|
|
1189
|
+
transportInterface: "macos",
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const candidates = buildCandidateList(ctx);
|
|
1193
|
+
|
|
1194
|
+
// cooldownMs=0 means never suppress
|
|
1195
|
+
expect(candidates.length).toBe(2);
|
|
1196
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
1197
|
+
expect(candidates[1].kind).toBe("local");
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// ── Cooldown recording on transport failures ───────────────────────
|
|
1201
|
+
|
|
1202
|
+
test("desktop-auto cdp-inspect transport failure records cooldown", async () => {
|
|
1203
|
+
// Make cdp-inspect fail with transport_error
|
|
1204
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
1205
|
+
(conversationId: string) => {
|
|
1206
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
1207
|
+
c.send = mock(async () => {
|
|
1208
|
+
throw new CdpError("transport_error", "Connection refused", {
|
|
1209
|
+
cdpMethod: "Page.navigate",
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
lastCdpInspectClient = c;
|
|
1213
|
+
return c;
|
|
1214
|
+
},
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
const ctx = makeContext({
|
|
1218
|
+
conversationId: "macos-cooldown-record",
|
|
1219
|
+
transportInterface: "macos",
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
const client = getCdpClient(ctx);
|
|
1223
|
+
|
|
1224
|
+
// First send: cdp-inspect fails, falls over to local
|
|
1225
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1226
|
+
"Page.navigate",
|
|
1227
|
+
);
|
|
1228
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
1229
|
+
|
|
1230
|
+
// Cooldown should now be active
|
|
1231
|
+
expect(_getDesktopAutoCooldownSince()).toBeGreaterThan(0);
|
|
1232
|
+
expect(isDesktopAutoCooldownActive(30_000)).toBe(true);
|
|
1233
|
+
|
|
1234
|
+
// Subsequent buildCandidateList should skip cdp-inspect
|
|
1235
|
+
client.dispose();
|
|
1236
|
+
const ctx2 = makeContext({
|
|
1237
|
+
conversationId: "macos-after-cooldown",
|
|
1238
|
+
transportInterface: "macos",
|
|
1239
|
+
});
|
|
1240
|
+
const candidates = buildCandidateList(ctx2);
|
|
1241
|
+
expect(candidates.length).toBe(1);
|
|
1242
|
+
expect(candidates[0].kind).toBe("local");
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
test("macOS turn with proxy unavailable routes to local without trying cdp-inspect", async () => {
|
|
1246
|
+
const fakeProxy = makeUnavailableProxy();
|
|
1247
|
+
const ctx = makeContext({
|
|
1248
|
+
conversationId: "macos-proxy-unavail-route",
|
|
1249
|
+
hostBrowserProxy: fakeProxy,
|
|
1250
|
+
transportInterface: "macos",
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const client = getCdpClient(ctx);
|
|
1254
|
+
|
|
1255
|
+
// Should go straight to local -- no cdp-inspect candidate inserted
|
|
1256
|
+
expect(client.kind).toBe("local");
|
|
1257
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1258
|
+
"Page.navigate",
|
|
1259
|
+
);
|
|
1260
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
1261
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
1262
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1263
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1264
|
+
client.dispose();
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
test("explicit config cdp-inspect failure does NOT record desktop-auto cooldown", async () => {
|
|
1268
|
+
cdpInspectEnabled = true;
|
|
1269
|
+
|
|
1270
|
+
// Make cdp-inspect fail with transport_error
|
|
1271
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
1272
|
+
(conversationId: string) => {
|
|
1273
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
1274
|
+
c.send = mock(async () => {
|
|
1275
|
+
throw new CdpError("transport_error", "Connection refused", {
|
|
1276
|
+
cdpMethod: "Page.navigate",
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
lastCdpInspectClient = c;
|
|
1280
|
+
return c;
|
|
1281
|
+
},
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
const ctx = makeContext({
|
|
1285
|
+
conversationId: "explicit-no-cooldown",
|
|
1286
|
+
transportInterface: "macos",
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
const client = getCdpClient(ctx);
|
|
1290
|
+
await client.send<{ ok: boolean; via: string }>("Page.navigate");
|
|
1291
|
+
client.dispose();
|
|
1292
|
+
|
|
1293
|
+
// Cooldown should NOT be recorded for explicit config candidates
|
|
1294
|
+
expect(_getDesktopAutoCooldownSince()).toBe(0);
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
// ── Cooldown utility function tests ─────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
test("isDesktopAutoCooldownActive returns false when no cooldown recorded", () => {
|
|
1300
|
+
expect(isDesktopAutoCooldownActive(30_000)).toBe(false);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
test("isDesktopAutoCooldownActive returns false when cooldownMs is 0", () => {
|
|
1304
|
+
recordDesktopAutoCooldown();
|
|
1305
|
+
expect(isDesktopAutoCooldownActive(0)).toBe(false);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
test("isDesktopAutoCooldownActive returns true within the window", () => {
|
|
1309
|
+
recordDesktopAutoCooldown();
|
|
1310
|
+
expect(isDesktopAutoCooldownActive(30_000)).toBe(true);
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
test("_resetDesktopAutoCooldown clears the cooldown", () => {
|
|
1314
|
+
recordDesktopAutoCooldown();
|
|
1315
|
+
expect(isDesktopAutoCooldownActive(30_000)).toBe(true);
|
|
1316
|
+
_resetDesktopAutoCooldown();
|
|
1317
|
+
expect(isDesktopAutoCooldownActive(30_000)).toBe(false);
|
|
1318
|
+
expect(_getDesktopAutoCooldownSince()).toBe(0);
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
// ── Pinned-mode tests ────────────────────────────────────────────────────
|
|
1323
|
+
|
|
1324
|
+
describe("pinned-mode selection", () => {
|
|
1325
|
+
beforeEach(() => {
|
|
1326
|
+
createExtensionCdpClientMock.mockClear();
|
|
1327
|
+
createLocalCdpClientMock.mockClear();
|
|
1328
|
+
createCdpInspectClientMock.mockClear();
|
|
1329
|
+
lastExtensionClient = undefined;
|
|
1330
|
+
lastLocalClient = undefined;
|
|
1331
|
+
lastCdpInspectClient = undefined;
|
|
1332
|
+
cdpInspectEnabled = false;
|
|
1333
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1334
|
+
_resetDesktopAutoCooldown();
|
|
1335
|
+
logWarnCalls.length = 0;
|
|
1336
|
+
logDebugCalls.length = 0;
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
// ── Pinned extension ────────────────────────────────────────────────
|
|
1340
|
+
|
|
1341
|
+
test("pinned extension mode routes to extension when proxy is available", async () => {
|
|
1342
|
+
const fakeProxy = makeAvailableProxy();
|
|
1343
|
+
const ctx = makeContext({
|
|
1344
|
+
conversationId: "pinned-ext",
|
|
1345
|
+
hostBrowserProxy: fakeProxy,
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
const client = getCdpClient(ctx, { mode: "extension" });
|
|
1349
|
+
expect(client.kind).toBe("extension");
|
|
1350
|
+
|
|
1351
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1352
|
+
"Page.navigate",
|
|
1353
|
+
);
|
|
1354
|
+
expect(result).toEqual({ ok: true, via: "extension" });
|
|
1355
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1356
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
1357
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
test("pinned extension mode throws when no proxy is provisioned", () => {
|
|
1361
|
+
const ctx = makeContext({ conversationId: "pinned-ext-no-proxy" });
|
|
1362
|
+
|
|
1363
|
+
expect(() => getCdpClient(ctx, { mode: "extension" })).toThrow(CdpError);
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
getCdpClient(ctx, { mode: "extension" });
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
expect(err).toBeInstanceOf(CdpError);
|
|
1369
|
+
const cdpErr = err as CdpError;
|
|
1370
|
+
expect(cdpErr.code).toBe("transport_error");
|
|
1371
|
+
expect(cdpErr.message).toContain('Pinned mode "extension" unavailable');
|
|
1372
|
+
expect(cdpErr.message).toContain("no host browser proxy provisioned");
|
|
1373
|
+
expect(cdpErr.attemptDiagnostics).toBeDefined();
|
|
1374
|
+
expect(cdpErr.attemptDiagnostics).toHaveLength(1);
|
|
1375
|
+
expect(cdpErr.attemptDiagnostics![0].candidateKind).toBe("extension");
|
|
1376
|
+
expect(cdpErr.attemptDiagnostics![0].stage).toBe("candidate_selection");
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
test("pinned extension mode throws when proxy is present but unavailable", () => {
|
|
1381
|
+
const fakeProxy = makeUnavailableProxy();
|
|
1382
|
+
const ctx = makeContext({
|
|
1383
|
+
conversationId: "pinned-ext-unavail",
|
|
1384
|
+
hostBrowserProxy: fakeProxy,
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
expect(() => getCdpClient(ctx, { mode: "extension" })).toThrow(CdpError);
|
|
1388
|
+
|
|
1389
|
+
try {
|
|
1390
|
+
getCdpClient(ctx, { mode: "extension" });
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
const cdpErr = err as CdpError;
|
|
1393
|
+
expect(cdpErr.code).toBe("transport_error");
|
|
1394
|
+
expect(cdpErr.message).toContain("not connected");
|
|
1395
|
+
expect(cdpErr.attemptDiagnostics![0].stage).toBe("candidate_selection");
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
test("pinned extension mode does NOT fall back to local on transport error", async () => {
|
|
1400
|
+
const fakeProxy = makeAvailableProxy();
|
|
1401
|
+
|
|
1402
|
+
// Make extension fail with transport_error
|
|
1403
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
1404
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
1405
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
1406
|
+
c.send = mock(async () => {
|
|
1407
|
+
throw new CdpError("transport_error", "WS disconnected");
|
|
1408
|
+
});
|
|
1409
|
+
lastExtensionClient = c;
|
|
1410
|
+
return c;
|
|
1411
|
+
},
|
|
1412
|
+
);
|
|
1413
|
+
|
|
1414
|
+
const ctx = makeContext({
|
|
1415
|
+
conversationId: "pinned-ext-no-fallback",
|
|
1416
|
+
hostBrowserProxy: fakeProxy,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
const client = getCdpClient(ctx, { mode: "extension" });
|
|
1420
|
+
|
|
1421
|
+
await expect(client.send("Page.navigate")).rejects.toMatchObject({
|
|
1422
|
+
code: "transport_error",
|
|
1423
|
+
message: "WS disconnected",
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// Local and cdp-inspect should NOT have been tried
|
|
1427
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
1428
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// ── Pinned cdp-inspect ──────────────────────────────────────────────
|
|
1432
|
+
|
|
1433
|
+
test("pinned cdp-inspect mode routes to cdp-inspect", async () => {
|
|
1434
|
+
const ctx = makeContext({ conversationId: "pinned-inspect" });
|
|
1435
|
+
|
|
1436
|
+
const client = getCdpClient(ctx, { mode: "cdp-inspect" });
|
|
1437
|
+
expect(client.kind).toBe("cdp-inspect");
|
|
1438
|
+
|
|
1439
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1440
|
+
"Page.navigate",
|
|
1441
|
+
);
|
|
1442
|
+
expect(result).toEqual({ ok: true, via: "cdp-inspect" });
|
|
1443
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
1444
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1445
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
test("pinned cdp-inspect mode does NOT fall back to local on transport error", async () => {
|
|
1449
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
1450
|
+
(conversationId: string) => {
|
|
1451
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
1452
|
+
c.send = mock(async () => {
|
|
1453
|
+
throw new CdpError("transport_error", "Connection refused");
|
|
1454
|
+
});
|
|
1455
|
+
lastCdpInspectClient = c;
|
|
1456
|
+
return c;
|
|
1457
|
+
},
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
const ctx = makeContext({ conversationId: "pinned-inspect-no-fb" });
|
|
1461
|
+
const client = getCdpClient(ctx, { mode: "cdp-inspect" });
|
|
1462
|
+
|
|
1463
|
+
await expect(client.send("Page.navigate")).rejects.toMatchObject({
|
|
1464
|
+
code: "transport_error",
|
|
1465
|
+
message: "Connection refused",
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
1469
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
test("pinned cdp-inspect uses config host/port", async () => {
|
|
1473
|
+
const ctx = makeContext({ conversationId: "pinned-inspect-cfg" });
|
|
1474
|
+
|
|
1475
|
+
const client = getCdpClient(ctx, { mode: "cdp-inspect" });
|
|
1476
|
+
await client.send("Page.navigate");
|
|
1477
|
+
|
|
1478
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledWith(
|
|
1479
|
+
"pinned-inspect-cfg",
|
|
1480
|
+
{
|
|
1481
|
+
host: "localhost",
|
|
1482
|
+
port: 9222,
|
|
1483
|
+
discoveryTimeoutMs: 500,
|
|
1484
|
+
},
|
|
1485
|
+
);
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
// ── Pinned local ────────────────────────────────────────────────────
|
|
1489
|
+
|
|
1490
|
+
test("pinned local mode routes to local", async () => {
|
|
1491
|
+
const fakeProxy = makeAvailableProxy();
|
|
1492
|
+
const ctx = makeContext({
|
|
1493
|
+
conversationId: "pinned-local",
|
|
1494
|
+
hostBrowserProxy: fakeProxy,
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
// Even with proxy available, pinned local should skip extension
|
|
1498
|
+
const client = getCdpClient(ctx, { mode: "local" });
|
|
1499
|
+
expect(client.kind).toBe("local");
|
|
1500
|
+
|
|
1501
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1502
|
+
"Runtime.evaluate",
|
|
1503
|
+
);
|
|
1504
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
1505
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1506
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1507
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
test("pinned local mode does NOT fall back on transport error", async () => {
|
|
1511
|
+
createLocalCdpClientMock.mockImplementationOnce(
|
|
1512
|
+
(conversationId: string) => {
|
|
1513
|
+
const c = makeFakeLocalClient(conversationId);
|
|
1514
|
+
c.send = mock(async () => {
|
|
1515
|
+
throw new CdpError("transport_error", "Playwright crashed");
|
|
1516
|
+
});
|
|
1517
|
+
lastLocalClient = c;
|
|
1518
|
+
return c;
|
|
1519
|
+
},
|
|
1520
|
+
);
|
|
1521
|
+
|
|
1522
|
+
const ctx = makeContext({ conversationId: "pinned-local-no-fb" });
|
|
1523
|
+
const client = getCdpClient(ctx, { mode: "local" });
|
|
1524
|
+
|
|
1525
|
+
await expect(client.send("Page.navigate")).rejects.toMatchObject({
|
|
1526
|
+
code: "transport_error",
|
|
1527
|
+
message: "Playwright crashed",
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1531
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
// ── buildPinnedCandidateList tests ───────────────────────────────────────
|
|
1536
|
+
|
|
1537
|
+
describe("buildPinnedCandidateList", () => {
|
|
1538
|
+
beforeEach(() => {
|
|
1539
|
+
createExtensionCdpClientMock.mockClear();
|
|
1540
|
+
createLocalCdpClientMock.mockClear();
|
|
1541
|
+
createCdpInspectClientMock.mockClear();
|
|
1542
|
+
lastExtensionClient = undefined;
|
|
1543
|
+
lastLocalClient = undefined;
|
|
1544
|
+
lastCdpInspectClient = undefined;
|
|
1545
|
+
cdpInspectEnabled = false;
|
|
1546
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1547
|
+
_resetDesktopAutoCooldown();
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
test("extension mode produces single extension candidate", () => {
|
|
1551
|
+
const fakeProxy = makeAvailableProxy();
|
|
1552
|
+
const ctx = makeContext({
|
|
1553
|
+
conversationId: "bpl-ext",
|
|
1554
|
+
hostBrowserProxy: fakeProxy,
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
const candidates = buildPinnedCandidateList(ctx, "extension");
|
|
1558
|
+
|
|
1559
|
+
expect(candidates).toHaveLength(1);
|
|
1560
|
+
expect(candidates[0].kind).toBe("extension");
|
|
1561
|
+
expect(candidates[0].reason).toBe("pinned mode: extension");
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
test("cdp-inspect mode produces single cdp-inspect candidate", () => {
|
|
1565
|
+
const ctx = makeContext({ conversationId: "bpl-inspect" });
|
|
1566
|
+
|
|
1567
|
+
const candidates = buildPinnedCandidateList(ctx, "cdp-inspect");
|
|
1568
|
+
|
|
1569
|
+
expect(candidates).toHaveLength(1);
|
|
1570
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
1571
|
+
expect(candidates[0].reason).toBe("pinned mode: cdp-inspect");
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
test("local mode produces single local candidate", () => {
|
|
1575
|
+
const ctx = makeContext({ conversationId: "bpl-local" });
|
|
1576
|
+
|
|
1577
|
+
const candidates = buildPinnedCandidateList(ctx, "local");
|
|
1578
|
+
|
|
1579
|
+
expect(candidates).toHaveLength(1);
|
|
1580
|
+
expect(candidates[0].kind).toBe("local");
|
|
1581
|
+
expect(candidates[0].reason).toBe("pinned mode: local");
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
test("extension mode throws with diagnostics when proxy absent", () => {
|
|
1585
|
+
const ctx = makeContext({ conversationId: "bpl-ext-absent" });
|
|
1586
|
+
|
|
1587
|
+
try {
|
|
1588
|
+
buildPinnedCandidateList(ctx, "extension");
|
|
1589
|
+
expect(true).toBe(false); // should not reach
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
expect(err).toBeInstanceOf(CdpError);
|
|
1592
|
+
const cdpErr = err as CdpError;
|
|
1593
|
+
expect(cdpErr.code).toBe("transport_error");
|
|
1594
|
+
expect(cdpErr.attemptDiagnostics).toHaveLength(1);
|
|
1595
|
+
expect(cdpErr.attemptDiagnostics![0]).toMatchObject({
|
|
1596
|
+
candidateKind: "extension",
|
|
1597
|
+
inclusionReason: "pinned mode: extension",
|
|
1598
|
+
stage: "candidate_selection",
|
|
1599
|
+
errorCode: "transport_error",
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
// ── Attempt diagnostics & fallback log tests ─────────────────────────────
|
|
1606
|
+
|
|
1607
|
+
describe("attempt diagnostics", () => {
|
|
1608
|
+
beforeEach(() => {
|
|
1609
|
+
createExtensionCdpClientMock.mockClear();
|
|
1610
|
+
createLocalCdpClientMock.mockClear();
|
|
1611
|
+
createCdpInspectClientMock.mockClear();
|
|
1612
|
+
lastExtensionClient = undefined;
|
|
1613
|
+
lastLocalClient = undefined;
|
|
1614
|
+
lastCdpInspectClient = undefined;
|
|
1615
|
+
cdpInspectEnabled = false;
|
|
1616
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1617
|
+
_resetDesktopAutoCooldown();
|
|
1618
|
+
logWarnCalls.length = 0;
|
|
1619
|
+
logDebugCalls.length = 0;
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test("exhausted candidates error includes full attempt diagnostics", async () => {
|
|
1623
|
+
cdpInspectEnabled = true;
|
|
1624
|
+
const fakeProxy = makeAvailableProxy();
|
|
1625
|
+
|
|
1626
|
+
// Make extension fail
|
|
1627
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
1628
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
1629
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
1630
|
+
c.send = mock(async () => {
|
|
1631
|
+
throw new CdpError("transport_error", "ext disconnected");
|
|
1632
|
+
});
|
|
1633
|
+
lastExtensionClient = c;
|
|
1634
|
+
return c;
|
|
1635
|
+
},
|
|
1636
|
+
);
|
|
1637
|
+
|
|
1638
|
+
// Make cdp-inspect fail
|
|
1639
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
1640
|
+
(conversationId: string) => {
|
|
1641
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
1642
|
+
c.send = mock(async () => {
|
|
1643
|
+
throw new CdpError("transport_error", "inspect refused");
|
|
1644
|
+
});
|
|
1645
|
+
lastCdpInspectClient = c;
|
|
1646
|
+
return c;
|
|
1647
|
+
},
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
// Make local fail too
|
|
1651
|
+
createLocalCdpClientMock.mockImplementationOnce(
|
|
1652
|
+
(conversationId: string) => {
|
|
1653
|
+
const c = makeFakeLocalClient(conversationId);
|
|
1654
|
+
c.send = mock(async () => {
|
|
1655
|
+
throw new CdpError("transport_error", "playwright dead");
|
|
1656
|
+
});
|
|
1657
|
+
lastLocalClient = c;
|
|
1658
|
+
return c;
|
|
1659
|
+
},
|
|
1660
|
+
);
|
|
1661
|
+
|
|
1662
|
+
const ctx = makeContext({
|
|
1663
|
+
conversationId: "diag-all-fail",
|
|
1664
|
+
hostBrowserProxy: fakeProxy,
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
const client = getCdpClient(ctx);
|
|
1668
|
+
|
|
1669
|
+
try {
|
|
1670
|
+
await client.send("Page.navigate");
|
|
1671
|
+
expect(true).toBe(false); // should not reach
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
expect(err).toBeInstanceOf(CdpError);
|
|
1674
|
+
const cdpErr = err as CdpError;
|
|
1675
|
+
expect(cdpErr.code).toBe("transport_error");
|
|
1676
|
+
expect(cdpErr.attemptDiagnostics).toBeDefined();
|
|
1677
|
+
expect(cdpErr.attemptDiagnostics).toHaveLength(3);
|
|
1678
|
+
|
|
1679
|
+
// First attempt: extension
|
|
1680
|
+
expect(cdpErr.attemptDiagnostics![0]).toMatchObject({
|
|
1681
|
+
candidateKind: "extension",
|
|
1682
|
+
stage: "send",
|
|
1683
|
+
errorCode: "transport_error",
|
|
1684
|
+
errorMessage: expect.stringContaining("ext disconnected"),
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
// Second attempt: cdp-inspect
|
|
1688
|
+
expect(cdpErr.attemptDiagnostics![1]).toMatchObject({
|
|
1689
|
+
candidateKind: "cdp-inspect",
|
|
1690
|
+
stage: "send",
|
|
1691
|
+
errorCode: "transport_error",
|
|
1692
|
+
errorMessage: expect.stringContaining("inspect refused"),
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
// Third attempt: local
|
|
1696
|
+
expect(cdpErr.attemptDiagnostics![2]).toMatchObject({
|
|
1697
|
+
candidateKind: "local",
|
|
1698
|
+
stage: "send",
|
|
1699
|
+
errorCode: "transport_error",
|
|
1700
|
+
errorMessage: expect.stringContaining("playwright dead"),
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
test("successful fallback still records diagnostics for failed candidates", async () => {
|
|
1706
|
+
const fakeProxy = makeAvailableProxy();
|
|
1707
|
+
|
|
1708
|
+
// Make extension fail
|
|
1709
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
1710
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
1711
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
1712
|
+
c.send = mock(async () => {
|
|
1713
|
+
throw new CdpError("transport_error", "ext down");
|
|
1714
|
+
});
|
|
1715
|
+
lastExtensionClient = c;
|
|
1716
|
+
return c;
|
|
1717
|
+
},
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
const ctx = makeContext({
|
|
1721
|
+
conversationId: "diag-partial",
|
|
1722
|
+
hostBrowserProxy: fakeProxy,
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
const client = getCdpClient(ctx);
|
|
1726
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1727
|
+
"Page.navigate",
|
|
1728
|
+
);
|
|
1729
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
1730
|
+
|
|
1731
|
+
// The fallback log should have been emitted with attempt data
|
|
1732
|
+
const fallbackLogs = logWarnCalls.filter(
|
|
1733
|
+
(c) =>
|
|
1734
|
+
typeof c.args[1] === "string" &&
|
|
1735
|
+
c.args[1].includes("auto-mode fallback"),
|
|
1736
|
+
);
|
|
1737
|
+
expect(fallbackLogs.length).toBeGreaterThan(0);
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
test("auto-mode fallback log includes candidate sequence and failure reasons", async () => {
|
|
1741
|
+
cdpInspectEnabled = true;
|
|
1742
|
+
const fakeProxy = makeAvailableProxy();
|
|
1743
|
+
|
|
1744
|
+
// Make extension fail
|
|
1745
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
1746
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
1747
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
1748
|
+
c.send = mock(async () => {
|
|
1749
|
+
throw new CdpError("transport_error", "WS closed");
|
|
1750
|
+
});
|
|
1751
|
+
lastExtensionClient = c;
|
|
1752
|
+
return c;
|
|
1753
|
+
},
|
|
1754
|
+
);
|
|
1755
|
+
|
|
1756
|
+
// Make cdp-inspect fail
|
|
1757
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
1758
|
+
(conversationId: string) => {
|
|
1759
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
1760
|
+
c.send = mock(async () => {
|
|
1761
|
+
throw new CdpError("transport_error", "no debugger");
|
|
1762
|
+
});
|
|
1763
|
+
lastCdpInspectClient = c;
|
|
1764
|
+
return c;
|
|
1765
|
+
},
|
|
1766
|
+
);
|
|
1767
|
+
|
|
1768
|
+
const ctx = makeContext({
|
|
1769
|
+
conversationId: "diag-log-shape",
|
|
1770
|
+
hostBrowserProxy: fakeProxy,
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
const client = getCdpClient(ctx);
|
|
1774
|
+
await client.send<{ ok: boolean; via: string }>("Page.navigate");
|
|
1775
|
+
|
|
1776
|
+
// Check that a warn-level log was emitted for the completed fallback
|
|
1777
|
+
const completedLogs = logWarnCalls.filter(
|
|
1778
|
+
(c) =>
|
|
1779
|
+
typeof c.args[1] === "string" &&
|
|
1780
|
+
c.args[1].includes("fallback completed"),
|
|
1781
|
+
);
|
|
1782
|
+
expect(completedLogs.length).toBe(1);
|
|
1783
|
+
|
|
1784
|
+
// Verify the log payload contains the expected structure
|
|
1785
|
+
const payload = completedLogs[0].args[0] as Record<string, unknown>;
|
|
1786
|
+
expect(payload.conversationId).toBe("diag-log-shape");
|
|
1787
|
+
expect(payload.stickyCandidate).toBe("local");
|
|
1788
|
+
expect(Array.isArray(payload.attemptSequence)).toBe(true);
|
|
1789
|
+
const seq = payload.attemptSequence as Array<Record<string, unknown>>;
|
|
1790
|
+
expect(seq.length).toBe(3); // extension, cdp-inspect, local
|
|
1791
|
+
expect(seq[0].kind).toBe("extension");
|
|
1792
|
+
expect(seq[0].errorCode).toBe("transport_error");
|
|
1793
|
+
expect(seq[1].kind).toBe("cdp-inspect");
|
|
1794
|
+
expect(seq[1].errorCode).toBe("transport_error");
|
|
1795
|
+
expect(seq[2].kind).toBe("local");
|
|
1796
|
+
expect(seq[2].stage).toBe("success");
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
test("pinned mode transport error includes attempt diagnostics on the thrown error", async () => {
|
|
1800
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
1801
|
+
(conversationId: string) => {
|
|
1802
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
1803
|
+
c.send = mock(async () => {
|
|
1804
|
+
throw new CdpError("transport_error", "Connection refused");
|
|
1805
|
+
});
|
|
1806
|
+
lastCdpInspectClient = c;
|
|
1807
|
+
return c;
|
|
1808
|
+
},
|
|
1809
|
+
);
|
|
1810
|
+
|
|
1811
|
+
const ctx = makeContext({ conversationId: "pinned-diag" });
|
|
1812
|
+
const client = getCdpClient(ctx, { mode: "cdp-inspect" });
|
|
1813
|
+
|
|
1814
|
+
try {
|
|
1815
|
+
await client.send("Page.navigate");
|
|
1816
|
+
expect(true).toBe(false); // should not reach
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
expect(err).toBeInstanceOf(CdpError);
|
|
1819
|
+
const cdpErr = err as CdpError;
|
|
1820
|
+
expect(cdpErr.attemptDiagnostics).toBeDefined();
|
|
1821
|
+
expect(cdpErr.attemptDiagnostics).toHaveLength(1);
|
|
1822
|
+
expect(cdpErr.attemptDiagnostics![0]).toMatchObject({
|
|
1823
|
+
candidateKind: "cdp-inspect",
|
|
1824
|
+
inclusionReason: "pinned mode: cdp-inspect",
|
|
1825
|
+
stage: "send",
|
|
1826
|
+
errorCode: "transport_error",
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
test("construction failure is recorded in attempt diagnostics", async () => {
|
|
1832
|
+
// Make the cdp-inspect client's create() throw
|
|
1833
|
+
createCdpInspectClientMock.mockImplementationOnce(() => {
|
|
1834
|
+
throw new Error("Config missing");
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
cdpInspectEnabled = true;
|
|
1838
|
+
const ctx = makeContext({ conversationId: "diag-construction" });
|
|
1839
|
+
const client = getCdpClient(ctx);
|
|
1840
|
+
|
|
1841
|
+
// cdp-inspect construction fails, falls back to local
|
|
1842
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1843
|
+
"Page.navigate",
|
|
1844
|
+
);
|
|
1845
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
test("cdp_error on single-candidate list includes diagnostics", async () => {
|
|
1849
|
+
createLocalCdpClientMock.mockImplementationOnce(
|
|
1850
|
+
(conversationId: string) => {
|
|
1851
|
+
const c = makeFakeLocalClient(conversationId);
|
|
1852
|
+
c.send = mock(async () => {
|
|
1853
|
+
throw new CdpError("cdp_error", "Protocol error -32000");
|
|
1854
|
+
});
|
|
1855
|
+
lastLocalClient = c;
|
|
1856
|
+
return c;
|
|
1857
|
+
},
|
|
1858
|
+
);
|
|
1859
|
+
|
|
1860
|
+
const ctx = makeContext({ conversationId: "diag-cdp-err" });
|
|
1861
|
+
const client = getCdpClient(ctx);
|
|
1862
|
+
|
|
1863
|
+
try {
|
|
1864
|
+
await client.send("Page.navigate");
|
|
1865
|
+
expect(true).toBe(false);
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
const cdpErr = err as CdpError;
|
|
1868
|
+
expect(cdpErr.code).toBe("cdp_error");
|
|
1869
|
+
expect(cdpErr.attemptDiagnostics).toBeDefined();
|
|
1870
|
+
expect(cdpErr.attemptDiagnostics).toHaveLength(1);
|
|
1871
|
+
expect(cdpErr.attemptDiagnostics![0]).toMatchObject({
|
|
1872
|
+
candidateKind: "local",
|
|
1873
|
+
stage: "send",
|
|
1874
|
+
errorCode: "cdp_error",
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
// ── No-fallback guarantees for pinned modes ──────────────────────────────
|
|
1881
|
+
|
|
1882
|
+
describe("no-fallback guarantees", () => {
|
|
1883
|
+
beforeEach(() => {
|
|
1884
|
+
createExtensionCdpClientMock.mockClear();
|
|
1885
|
+
createLocalCdpClientMock.mockClear();
|
|
1886
|
+
createCdpInspectClientMock.mockClear();
|
|
1887
|
+
lastExtensionClient = undefined;
|
|
1888
|
+
lastLocalClient = undefined;
|
|
1889
|
+
lastCdpInspectClient = undefined;
|
|
1890
|
+
cdpInspectEnabled = false;
|
|
1891
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1892
|
+
_resetDesktopAutoCooldown();
|
|
1893
|
+
logWarnCalls.length = 0;
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
test("pinned extension: only one candidate is ever constructed", async () => {
|
|
1897
|
+
const fakeProxy = makeAvailableProxy();
|
|
1898
|
+
|
|
1899
|
+
// Make extension fail
|
|
1900
|
+
createExtensionCdpClientMock.mockImplementationOnce(
|
|
1901
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
1902
|
+
const c = makeFakeExtensionClient(conversationId);
|
|
1903
|
+
c.send = mock(async () => {
|
|
1904
|
+
throw new CdpError("transport_error", "failed");
|
|
1905
|
+
});
|
|
1906
|
+
lastExtensionClient = c;
|
|
1907
|
+
return c;
|
|
1908
|
+
},
|
|
1909
|
+
);
|
|
1910
|
+
|
|
1911
|
+
const ctx = makeContext({
|
|
1912
|
+
conversationId: "nofb-ext",
|
|
1913
|
+
hostBrowserProxy: fakeProxy,
|
|
1914
|
+
});
|
|
1915
|
+
const client = getCdpClient(ctx, { mode: "extension" });
|
|
1916
|
+
|
|
1917
|
+
await expect(client.send("Page.navigate")).rejects.toThrow();
|
|
1918
|
+
|
|
1919
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1920
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
1921
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
test("pinned cdp-inspect: only one candidate is ever constructed", async () => {
|
|
1925
|
+
createCdpInspectClientMock.mockImplementationOnce(
|
|
1926
|
+
(conversationId: string) => {
|
|
1927
|
+
const c = makeFakeCdpInspectClient(conversationId);
|
|
1928
|
+
c.send = mock(async () => {
|
|
1929
|
+
throw new CdpError("transport_error", "failed");
|
|
1930
|
+
});
|
|
1931
|
+
lastCdpInspectClient = c;
|
|
1932
|
+
return c;
|
|
1933
|
+
},
|
|
1934
|
+
);
|
|
1935
|
+
|
|
1936
|
+
const ctx = makeContext({ conversationId: "nofb-inspect" });
|
|
1937
|
+
const client = getCdpClient(ctx, { mode: "cdp-inspect" });
|
|
1938
|
+
|
|
1939
|
+
await expect(client.send("Page.navigate")).rejects.toThrow();
|
|
1940
|
+
|
|
1941
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
1942
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1943
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
test("pinned local: only one candidate is ever constructed", async () => {
|
|
1947
|
+
createLocalCdpClientMock.mockImplementationOnce(
|
|
1948
|
+
(conversationId: string) => {
|
|
1949
|
+
const c = makeFakeLocalClient(conversationId);
|
|
1950
|
+
c.send = mock(async () => {
|
|
1951
|
+
throw new CdpError("transport_error", "failed");
|
|
1952
|
+
});
|
|
1953
|
+
lastLocalClient = c;
|
|
1954
|
+
return c;
|
|
1955
|
+
},
|
|
1956
|
+
);
|
|
1957
|
+
|
|
1958
|
+
const ctx = makeContext({ conversationId: "nofb-local" });
|
|
1959
|
+
const client = getCdpClient(ctx, { mode: "local" });
|
|
1960
|
+
|
|
1961
|
+
await expect(client.send("Page.navigate")).rejects.toThrow();
|
|
1962
|
+
|
|
1963
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1964
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1965
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
test("pinned modes do not emit auto-mode fallback logs", async () => {
|
|
1969
|
+
createLocalCdpClientMock.mockImplementationOnce(
|
|
1970
|
+
(conversationId: string) => {
|
|
1971
|
+
const c = makeFakeLocalClient(conversationId);
|
|
1972
|
+
c.send = mock(async () => {
|
|
1973
|
+
throw new CdpError("transport_error", "failed");
|
|
1974
|
+
});
|
|
1975
|
+
lastLocalClient = c;
|
|
1976
|
+
return c;
|
|
1977
|
+
},
|
|
1978
|
+
);
|
|
1979
|
+
|
|
1980
|
+
const ctx = makeContext({ conversationId: "nofb-no-log" });
|
|
1981
|
+
const client = getCdpClient(ctx, { mode: "local" });
|
|
1982
|
+
|
|
1983
|
+
await expect(client.send("Page.navigate")).rejects.toThrow();
|
|
1984
|
+
|
|
1985
|
+
// No warn-level fallback logs should have been emitted
|
|
1986
|
+
const fallbackLogs = logWarnCalls.filter(
|
|
1987
|
+
(c) =>
|
|
1988
|
+
typeof c.args[1] === "string" &&
|
|
1989
|
+
c.args[1].includes("auto-mode fallback"),
|
|
1990
|
+
);
|
|
1991
|
+
expect(fallbackLogs.length).toBe(0);
|
|
1992
|
+
});
|
|
377
1993
|
});
|