@vellumai/assistant 0.6.2 → 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 +41 -49
- package/bunfig.toml +3 -0
- package/docs/architecture/memory.md +1 -1
- 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/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
- package/openapi.yaml +1111 -86
- package/package.json +40 -42
- package/scripts/generate-openapi.ts +0 -2
- package/scripts/test.sh +73 -18
- package/src/__tests__/acp-session.test.ts +43 -0
- 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__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +1 -0
- package/src/__tests__/app-source-watcher.test.ts +37 -11
- package/src/__tests__/approval-routes-http.test.ts +178 -1
- 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 +240 -94
- package/src/__tests__/browser-manager.test.ts +40 -27
- 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 +1000 -0
- package/src/__tests__/channel-approvals.test.ts +53 -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-managed-gemini-defaults.test.ts +326 -0
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +1248 -224
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-overflow-approval.test.ts +16 -1
- 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 +2 -1
- package/src/__tests__/conversation-agent-loop.test.ts +99 -3
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-fork-crud.test.ts +17 -0
- package/src/__tests__/conversation-history-web-search.test.ts +12 -4
- package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-inject-context.test.ts +103 -0
- 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 +946 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
- 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-starter-routes.test.ts +126 -0
- package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
- package/src/__tests__/conversation-store.test.ts +195 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -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-execution-approval-bridge.test.ts +32 -1
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +6 -3
- package/src/__tests__/credential-vault-unit.test.ts +383 -7
- package/src/__tests__/credential-vault.test.ts +152 -13
- package/src/__tests__/credentials-cli.test.ts +42 -18
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/date-context.test.ts +4 -4
- 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__/embedding-managed-proxy-selection.test.ts +256 -0
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +2 -0
- package/src/__tests__/gemini-provider.test.ts +66 -2
- 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__/guardian-routing-invariants.test.ts +70 -2
- package/src/__tests__/headless-browser-interactions.test.ts +738 -359
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +528 -49
- package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
- package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
- 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 +145 -1
- package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
- package/src/__tests__/host-browser-event-routes.test.ts +350 -0
- package/src/__tests__/host-browser-proxy.test.ts +444 -0
- package/src/__tests__/host-browser-routes.test.ts +198 -0
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
- package/src/__tests__/host-cu-proxy.test.ts +166 -1
- package/src/__tests__/host-file-proxy.test.ts +185 -1
- package/src/__tests__/host-file-read-tool.test.ts +52 -0
- package/src/__tests__/host-proxy-interface.test.ts +165 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -11
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- 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__/mcp-client-auth.test.ts +40 -4
- package/src/__tests__/mcp-health-check.test.ts +10 -3
- 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-cross-version-compatibility.test.ts +3 -1
- package/src/__tests__/migration-export-http.test.ts +67 -8
- package/src/__tests__/migration-export-streaming.test.ts +66 -0
- package/src/__tests__/migration-import-commit-http.test.ts +109 -7
- 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__/native-host-marker-sync-guard.test.ts +157 -0
- package/src/__tests__/oauth-apps-routes.test.ts +18 -12
- package/src/__tests__/oauth-cli.test.ts +709 -60
- package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
- package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
- package/src/__tests__/oauth-providers-routes.test.ts +52 -14
- package/src/__tests__/oauth-store.test.ts +1465 -176
- package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
- package/src/__tests__/onboarding-template-contract.test.ts +81 -70
- package/src/__tests__/openai-provider.test.ts +178 -2
- 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-categories.test.ts +1 -1
- package/src/__tests__/outlook-client-automation.test.ts +1 -1
- package/src/__tests__/outlook-compose-tools.test.ts +1 -1
- package/src/__tests__/outlook-email-watcher.test.ts +1 -1
- package/src/__tests__/outlook-follow-up.test.ts +1 -1
- package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
- package/src/__tests__/outlook-trash.test.ts +1 -1
- package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
- package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
- package/src/__tests__/permission-mode.test.ts +28 -56
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform-callback-registration.test.ts +19 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/proxy-approval-callback.test.ts +18 -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__/require-fresh-approval.test.ts +40 -1
- package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
- package/src/__tests__/schedule-routes.test.ts +162 -0
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-detection-handler.test.ts +84 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -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 +8 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/set-permission-mode.test.ts +13 -250
- 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 +801 -0
- package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
- 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 +576 -16
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/subagent-detail.test.ts +44 -2
- package/src/__tests__/subagent-disposal.test.ts +1 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
- package/src/__tests__/subagent-manager-notify.test.ts +1 -0
- package/src/__tests__/subagent-notify-parent.test.ts +1 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/subagent-types.test.ts +1 -0
- package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
- package/src/__tests__/system-prompt.test.ts +184 -27
- package/src/__tests__/task-scheduler.test.ts +32 -6
- package/src/__tests__/telegram-config.test.ts +10 -13
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +25 -5
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- 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__/tool-side-effects-slack-dm.test.ts +22 -0
- package/src/__tests__/top-level-renderer.test.ts +73 -1
- package/src/__tests__/transport-hints-queue.test.ts +14 -29
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- 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__/v2-consent-policy.test.ts +103 -0
- 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/acp/client-handler.ts +30 -4
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +55 -9
- package/src/approvals/guardian-request-resolvers.ts +21 -15
- 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/browser-session/__tests__/manager.test.ts +297 -0
- package/src/browser-session/backends/cdp-inspect.ts +30 -0
- package/src/browser-session/backends/extension.ts +26 -0
- package/src/browser-session/backends/local.ts +24 -0
- package/src/browser-session/events.ts +164 -0
- package/src/browser-session/index.ts +27 -0
- package/src/browser-session/manager.ts +159 -0
- package/src/browser-session/types.ts +28 -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/__tests__/types.test.ts +134 -0
- package/src/channels/types.ts +69 -3
- 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 +3 -4
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +273 -16
- package/src/cli/commands/mcp.ts +16 -4
- package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
- package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
- package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
- package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
- package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
- package/src/cli/commands/oauth/apps.ts +7 -4
- package/src/cli/commands/oauth/connect.ts +6 -3
- package/src/cli/commands/oauth/disconnect.ts +1 -1
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +215 -36
- package/src/cli/commands/oauth/shared.ts +7 -6
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
- 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/commands/platform/index.ts +107 -10
- package/src/cli/commands/usage.ts +10 -9
- package/src/cli/lib/daemon-credential-client.ts +4 -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/SKILL.md +26 -249
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
- package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
- 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 +5 -2
- package/src/config/bundled-skills/document/SKILL.md +4 -0
- package/src/config/bundled-skills/gmail/SKILL.md +54 -8
- 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 +9 -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/subagent/SKILL.md +21 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
- package/src/config/bundled-skills/tasks/SKILL.md +5 -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 +38 -0
- package/src/config/env.ts +49 -4
- package/src/config/feature-flag-registry.json +85 -14
- package/src/config/loader.ts +82 -13
- package/src/config/sanitize-for-transfer.ts +47 -0
- package/src/config/schema.ts +81 -15
- 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 +112 -0
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/memory-retrieval.ts +103 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +52 -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 -1
- 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 +177 -0
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +61 -10
- package/src/credential-execution/approval-bridge.ts +49 -15
- 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 +195 -0
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/context-overflow-approval.ts +5 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
- package/src/daemon/conversation-agent-loop.ts +153 -42
- package/src/daemon/conversation-attachments.ts +40 -0
- 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 +622 -13
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +128 -36
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +131 -40
- package/src/daemon/conversation-tool-setup.ts +99 -8
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +292 -16
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +13 -141
- package/src/daemon/handlers/shared.ts +80 -0
- package/src/daemon/handlers/skills.ts +483 -44
- package/src/daemon/host-bash-proxy.ts +48 -13
- package/src/daemon/host-browser-proxy.ts +192 -0
- package/src/daemon/host-cu-proxy.ts +36 -11
- package/src/daemon/host-file-proxy.ts +57 -9
- package/src/daemon/lifecycle.ts +179 -28
- package/src/daemon/message-protocol.ts +13 -0
- package/src/daemon/message-types/conversations.ts +89 -14
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +19 -5
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +26 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +439 -14
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/daemon/transport-hints.ts +5 -24
- 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 +30 -20
- 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/mcp/client.ts +59 -24
- 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 +31 -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 +122 -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/conversation-starters-cadence.ts +76 -0
- package/src/memory/conversation-title-service.ts +5 -2
- package/src/memory/db-init.ts +18 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/embedding-backend.test.ts +75 -0
- package/src/memory/embedding-backend.ts +131 -5
- package/src/memory/embedding-gemini.test.ts +54 -0
- package/src/memory/embedding-gemini.ts +20 -9
- package/src/memory/embedding-local.ts +176 -17
- package/src/memory/graph/consolidation.ts +10 -23
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction-job.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 +67 -40
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/store.test.ts +7 -3
- package/src/memory/graph/store.ts +47 -12
- 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 +137 -60
- package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
- package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
- package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
- package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
- package/src/memory/migrations/217-conversation-host-access.ts +40 -0
- package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
- 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 +12 -0
- package/src/memory/migrations/registry.ts +16 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/oauth.ts +21 -13
- 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/AGENTS.md +76 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
- package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
- package/src/oauth/byo-connection.test.ts +26 -9
- package/src/oauth/byo-connection.ts +10 -8
- package/src/oauth/connect-orchestrator.ts +25 -21
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/connection-resolver.test.ts +17 -4
- package/src/oauth/connection-resolver.ts +22 -18
- package/src/oauth/connection.ts +3 -1
- package/src/oauth/manual-token-connection.ts +13 -13
- package/src/oauth/oauth-store.ts +223 -100
- package/src/oauth/platform-connection.test.ts +101 -3
- package/src/oauth/platform-connection.ts +56 -35
- package/src/oauth/provider-serializer.ts +31 -5
- package/src/oauth/revoke.ts +76 -0
- package/src/oauth/seed-providers.ts +133 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/v2-consent-policy.ts +87 -0
- 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 +76 -38
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -105
- 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 -60
- 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 +10 -1
- package/src/runtime/AGENTS.md +65 -0
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
- package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
- package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -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/assistant-event-hub.ts +2 -2
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
- package/src/runtime/auth/middleware.ts +98 -0
- package/src/runtime/auth/route-policy.ts +33 -9
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +414 -0
- package/src/runtime/channel-approvals.ts +18 -5
- 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 +368 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
- package/src/runtime/guardian-decision-types.ts +7 -0
- package/src/runtime/http-server.ts +815 -75
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +7 -0
- package/src/runtime/migrations/migration-wizard.ts +23 -2
- package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
- package/src/runtime/migrations/vbundle-builder.ts +145 -38
- package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
- package/src/runtime/migrations/vbundle-importer.ts +89 -5
- package/src/runtime/pending-interactions.ts +18 -13
- 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/approval-routes.ts +90 -16
- 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 +556 -0
- 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 -141
- package/src/runtime/routes/conversation-management-routes.ts +223 -0
- package/src/runtime/routes/conversation-routes.ts +598 -103
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/guardian-action-routes.ts +24 -13
- 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 +268 -0
- package/src/runtime/routes/host-file-routes.ts +9 -1
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +262 -33
- 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/log-export-routes.ts +42 -22
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/memory-item-routes.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +122 -2
- package/src/runtime/routes/oauth-apps.ts +15 -17
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/schedule-routes.ts +24 -11
- package/src/runtime/routes/settings-routes.ts +31 -102
- package/src/runtime/routes/skills-routes.ts +128 -9
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/subagents-routes.ts +14 -10
- 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 +38 -9
- 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/routes/workspace-routes.test.ts +22 -0
- package/src/runtime/routes/workspace-routes.ts +8 -1
- package/src/runtime/routes/workspace-utils.ts +2 -0
- 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 +57 -5
- package/src/security/ces-credential-client.ts +20 -0
- package/src/security/ces-rpc-credential-backend.ts +17 -0
- package/src/security/credential-backend.ts +5 -0
- package/src/security/oauth2.ts +68 -29
- package/src/security/secure-keys.ts +143 -27
- package/src/security/token-manager.ts +31 -10
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +554 -0
- 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 +169 -40
- package/src/subagent/types.ts +19 -0
- package/src/tools/apps/executors.ts +11 -2
- package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
- 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/auth-detector.ts +43 -12
- package/src/tools/browser/browser-execution.ts +1787 -342
- package/src/tools/browser/browser-manager.ts +81 -12
- 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__/accessibility-snapshot.test.ts +318 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
- package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
- package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
- package/src/tools/browser/cdp-client/errors.ts +49 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
- package/src/tools/browser/cdp-client/factory.ts +914 -0
- package/src/tools/browser/cdp-client/index.ts +28 -0
- package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
- package/src/tools/browser/cdp-client/types.ts +120 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +1 -1
- package/src/tools/host-filesystem/read.ts +12 -15
- package/src/tools/host-filesystem/write.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +21 -16
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/permission-checker.ts +77 -82
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -0
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- 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/subagent/spawn.ts +47 -3
- package/src/tools/subagent/status.ts +2 -0
- package/src/tools/system/register.ts +2 -16
- package/src/tools/terminal/safe-env.ts +15 -0
- package/src/tools/terminal/shell.ts +36 -20
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +19 -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 +63 -24
- 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 +31 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/cli/commands/browser-relay.ts +0 -536
- package/src/config/schemas/sandbox.ts +0 -14
- 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/permissions/permission-mode-store.ts +0 -180
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/tools/browser/chrome-cdp.ts +0 -239
- package/src/tools/system/set-permission-mode.ts +0 -103
|
@@ -0,0 +1,1993 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { HostBrowserProxy } from "../../../../daemon/host-browser-proxy.js";
|
|
4
|
+
import type { ToolContext } from "../../../types.js";
|
|
5
|
+
import { CdpError } from "../errors.js";
|
|
6
|
+
|
|
7
|
+
type FakeClient = {
|
|
8
|
+
kind: "extension" | "local" | "cdp-inspect";
|
|
9
|
+
conversationId: string;
|
|
10
|
+
send: ReturnType<typeof mock>;
|
|
11
|
+
dispose: ReturnType<typeof mock>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function makeFakeExtensionClient(conversationId: string): FakeClient {
|
|
15
|
+
return {
|
|
16
|
+
kind: "extension",
|
|
17
|
+
conversationId,
|
|
18
|
+
send: mock(async () => ({ ok: true, via: "extension" })),
|
|
19
|
+
dispose: mock(() => {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeFakeLocalClient(conversationId: string): FakeClient {
|
|
24
|
+
return {
|
|
25
|
+
kind: "local",
|
|
26
|
+
conversationId,
|
|
27
|
+
send: mock(async () => ({ ok: true, via: "local" })),
|
|
28
|
+
dispose: mock(() => {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeFakeCdpInspectClient(conversationId: string): FakeClient {
|
|
33
|
+
return {
|
|
34
|
+
kind: "cdp-inspect",
|
|
35
|
+
conversationId,
|
|
36
|
+
send: mock(async () => ({ ok: true, via: "cdp-inspect" })),
|
|
37
|
+
dispose: mock(() => {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let lastExtensionClient: FakeClient | undefined;
|
|
42
|
+
let lastLocalClient: FakeClient | undefined;
|
|
43
|
+
let lastCdpInspectClient: FakeClient | undefined;
|
|
44
|
+
|
|
45
|
+
const createExtensionCdpClientMock = mock(
|
|
46
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
47
|
+
const client = makeFakeExtensionClient(conversationId);
|
|
48
|
+
lastExtensionClient = client;
|
|
49
|
+
return client;
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const createLocalCdpClientMock = mock((conversationId: string) => {
|
|
54
|
+
const client = makeFakeLocalClient(conversationId);
|
|
55
|
+
lastLocalClient = client;
|
|
56
|
+
return client;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const createCdpInspectClientMock = mock(
|
|
60
|
+
(conversationId: string, _options: unknown) => {
|
|
61
|
+
const client = makeFakeCdpInspectClient(conversationId);
|
|
62
|
+
lastCdpInspectClient = client;
|
|
63
|
+
return client;
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/**
|
|
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
|
+
*/
|
|
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[] }> = [];
|
|
80
|
+
|
|
81
|
+
mock.module("../extension-cdp-client.js", () => ({
|
|
82
|
+
createExtensionCdpClient: createExtensionCdpClientMock,
|
|
83
|
+
}));
|
|
84
|
+
mock.module("../local-cdp-client.js", () => ({
|
|
85
|
+
createLocalCdpClient: createLocalCdpClientMock,
|
|
86
|
+
}));
|
|
87
|
+
mock.module("../cdp-inspect-client.js", () => ({
|
|
88
|
+
createCdpInspectClient: createCdpInspectClientMock,
|
|
89
|
+
}));
|
|
90
|
+
mock.module("../../../../config/loader.js", () => ({
|
|
91
|
+
getConfig: () => ({
|
|
92
|
+
hostBrowser: {
|
|
93
|
+
cdpInspect: {
|
|
94
|
+
enabled: cdpInspectEnabled,
|
|
95
|
+
host: "localhost",
|
|
96
|
+
port: 9222,
|
|
97
|
+
probeTimeoutMs: 500,
|
|
98
|
+
desktopAuto: desktopAutoConfig,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
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
|
+
}));
|
|
115
|
+
|
|
116
|
+
// Import under test AFTER mock.module calls so that the factory's
|
|
117
|
+
// top-level imports resolve to our fakes.
|
|
118
|
+
const {
|
|
119
|
+
getCdpClient,
|
|
120
|
+
buildCandidateList,
|
|
121
|
+
buildChainedClient,
|
|
122
|
+
buildPinnedCandidateList,
|
|
123
|
+
_resetDesktopAutoCooldown,
|
|
124
|
+
_getDesktopAutoCooldownSince,
|
|
125
|
+
recordDesktopAutoCooldown,
|
|
126
|
+
isDesktopAutoCooldownActive,
|
|
127
|
+
} = await import("../factory.js");
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Minimal ToolContext suitable for factory tests. Only the fields the
|
|
131
|
+
* factory reads (`conversationId` and `hostBrowserProxy`) need to be
|
|
132
|
+
* populated; other required fields are cast away.
|
|
133
|
+
*/
|
|
134
|
+
function makeContext(
|
|
135
|
+
overrides: Partial<ToolContext> & { conversationId: string },
|
|
136
|
+
): ToolContext {
|
|
137
|
+
return overrides as unknown as ToolContext;
|
|
138
|
+
}
|
|
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
|
+
|
|
161
|
+
describe("getCdpClient", () => {
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
createExtensionCdpClientMock.mockClear();
|
|
164
|
+
createLocalCdpClientMock.mockClear();
|
|
165
|
+
createCdpInspectClientMock.mockClear();
|
|
166
|
+
lastExtensionClient = undefined;
|
|
167
|
+
lastLocalClient = undefined;
|
|
168
|
+
lastCdpInspectClient = undefined;
|
|
169
|
+
cdpInspectEnabled = false;
|
|
170
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
171
|
+
_resetDesktopAutoCooldown();
|
|
172
|
+
logWarnCalls.length = 0;
|
|
173
|
+
logDebugCalls.length = 0;
|
|
174
|
+
});
|
|
175
|
+
|
|
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();
|
|
180
|
+
const ctx = makeContext({
|
|
181
|
+
conversationId: "test-convo",
|
|
182
|
+
hostBrowserProxy: fakeProxy,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const client = getCdpClient(ctx);
|
|
186
|
+
|
|
187
|
+
// kind should reflect extension before first send (top candidate)
|
|
188
|
+
expect(client.kind).toBe("extension");
|
|
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" });
|
|
197
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
198
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledWith(
|
|
199
|
+
fakeProxy,
|
|
200
|
+
"test-convo",
|
|
201
|
+
);
|
|
202
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
203
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
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 () => {
|
|
248
|
+
cdpInspectEnabled = true;
|
|
249
|
+
const fakeProxy = makeAvailableProxy();
|
|
250
|
+
const ctx = makeContext({
|
|
251
|
+
conversationId: "ext-wins",
|
|
252
|
+
hostBrowserProxy: fakeProxy,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const client = getCdpClient(ctx);
|
|
256
|
+
|
|
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" });
|
|
262
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
263
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
264
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("routes to CdpInspectClient when cdpInspect is enabled and extension is absent", async () => {
|
|
268
|
+
cdpInspectEnabled = true;
|
|
269
|
+
const ctx = makeContext({
|
|
270
|
+
conversationId: "inspect-convo",
|
|
271
|
+
hostBrowserProxy: undefined,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const client = getCdpClient(ctx);
|
|
275
|
+
|
|
276
|
+
expect(client.kind).toBe("cdp-inspect");
|
|
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" });
|
|
284
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
285
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledWith("inspect-convo", {
|
|
286
|
+
host: "localhost",
|
|
287
|
+
port: 9222,
|
|
288
|
+
discoveryTimeoutMs: 500,
|
|
289
|
+
});
|
|
290
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
291
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("routes to LocalCdpClient when cdpInspect is disabled and extension is absent", async () => {
|
|
295
|
+
cdpInspectEnabled = false;
|
|
296
|
+
const ctx = makeContext({
|
|
297
|
+
conversationId: "local-convo",
|
|
298
|
+
hostBrowserProxy: undefined,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const client = getCdpClient(ctx);
|
|
302
|
+
|
|
303
|
+
expect(client.kind).toBe("local");
|
|
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" });
|
|
311
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
312
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledWith("local-convo");
|
|
313
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
314
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("routes to LocalCdpClient when hostBrowserProxy key is omitted", async () => {
|
|
318
|
+
const ctx = makeContext({ conversationId: "another-convo" });
|
|
319
|
+
|
|
320
|
+
const client = getCdpClient(ctx);
|
|
321
|
+
|
|
322
|
+
expect(client.kind).toBe("local");
|
|
323
|
+
expect(client.conversationId).toBe("another-convo");
|
|
324
|
+
|
|
325
|
+
await client.send("Runtime.evaluate");
|
|
326
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
327
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledWith("another-convo");
|
|
328
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
329
|
+
expect(createCdpInspectClientMock).not.toHaveBeenCalled();
|
|
330
|
+
});
|
|
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
|
+
|
|
362
|
+
test("forwards send() through the manager to the extension-backed client", async () => {
|
|
363
|
+
const fakeProxy = makeAvailableProxy();
|
|
364
|
+
const ctx = makeContext({
|
|
365
|
+
conversationId: "send-ext",
|
|
366
|
+
hostBrowserProxy: fakeProxy,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const client = getCdpClient(ctx);
|
|
370
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
371
|
+
"Page.navigate",
|
|
372
|
+
{ url: "https://example.com" },
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
expect(result).toEqual({ ok: true, via: "extension" });
|
|
376
|
+
expect(lastExtensionClient?.send).toHaveBeenCalledTimes(1);
|
|
377
|
+
expect(lastExtensionClient?.send).toHaveBeenCalledWith(
|
|
378
|
+
"Page.navigate",
|
|
379
|
+
{ url: "https://example.com" },
|
|
380
|
+
undefined,
|
|
381
|
+
);
|
|
382
|
+
expect(lastLocalClient).toBeUndefined();
|
|
383
|
+
expect(lastCdpInspectClient).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("forwards send() through the manager to the local-backed client", async () => {
|
|
387
|
+
const ctx = makeContext({ conversationId: "send-local" });
|
|
388
|
+
|
|
389
|
+
const client = getCdpClient(ctx);
|
|
390
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
391
|
+
"Runtime.evaluate",
|
|
392
|
+
{ expression: "1+1" },
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
396
|
+
expect(lastLocalClient?.send).toHaveBeenCalledTimes(1);
|
|
397
|
+
expect(lastLocalClient?.send).toHaveBeenCalledWith(
|
|
398
|
+
"Runtime.evaluate",
|
|
399
|
+
{ expression: "1+1" },
|
|
400
|
+
undefined,
|
|
401
|
+
);
|
|
402
|
+
expect(lastExtensionClient).toBeUndefined();
|
|
403
|
+
expect(lastCdpInspectClient).toBeUndefined();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("forwards send() through the manager to the cdp-inspect-backed client", async () => {
|
|
407
|
+
cdpInspectEnabled = true;
|
|
408
|
+
const ctx = makeContext({ conversationId: "send-inspect" });
|
|
409
|
+
|
|
410
|
+
const client = getCdpClient(ctx);
|
|
411
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
412
|
+
"Page.navigate",
|
|
413
|
+
{ url: "https://example.com" },
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(result).toEqual({ ok: true, via: "cdp-inspect" });
|
|
417
|
+
expect(lastCdpInspectClient?.send).toHaveBeenCalledTimes(1);
|
|
418
|
+
expect(lastCdpInspectClient?.send).toHaveBeenCalledWith(
|
|
419
|
+
"Page.navigate",
|
|
420
|
+
{ url: "https://example.com" },
|
|
421
|
+
undefined,
|
|
422
|
+
);
|
|
423
|
+
expect(lastExtensionClient).toBeUndefined();
|
|
424
|
+
expect(lastLocalClient).toBeUndefined();
|
|
425
|
+
});
|
|
426
|
+
|
|
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" });
|
|
432
|
+
const client = getCdpClient(ctx);
|
|
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
|
+
);
|
|
447
|
+
|
|
448
|
+
await expect(
|
|
449
|
+
client.send("Page.navigate", { url: "https://example.com" }),
|
|
450
|
+
).rejects.toMatchObject({ code: "cdp_error", message: "kaboom" });
|
|
451
|
+
|
|
452
|
+
// Should NOT have fallen through to local
|
|
453
|
+
expect(createLocalCdpClientMock).not.toHaveBeenCalled();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("propagates caller AbortSignal to the underlying client", async () => {
|
|
457
|
+
const ctx = makeContext({ conversationId: "abort-local" });
|
|
458
|
+
const client = getCdpClient(ctx);
|
|
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
|
+
|
|
464
|
+
let sawSignal: AbortSignal | undefined;
|
|
465
|
+
lastLocalClient!.send = mock(
|
|
466
|
+
async (
|
|
467
|
+
_method: string,
|
|
468
|
+
_params?: Record<string, unknown>,
|
|
469
|
+
signal?: AbortSignal,
|
|
470
|
+
) => {
|
|
471
|
+
sawSignal = signal;
|
|
472
|
+
if (signal?.aborted) {
|
|
473
|
+
throw new CdpError("aborted", "aborted before send");
|
|
474
|
+
}
|
|
475
|
+
return {};
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
controller.abort();
|
|
480
|
+
await expect(
|
|
481
|
+
client.send("Page.navigate", { url: "https://x" }, controller.signal),
|
|
482
|
+
).rejects.toMatchObject({ code: "aborted" });
|
|
483
|
+
expect(sawSignal).toBe(controller.signal);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ── Dispose ──────────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
test("dispose() tears down the underlying client and rejects further sends", async () => {
|
|
489
|
+
const ctx = makeContext({ conversationId: "dispose-local" });
|
|
490
|
+
const client = getCdpClient(ctx);
|
|
491
|
+
|
|
492
|
+
// Trigger client creation via send
|
|
493
|
+
await client.send("Runtime.evaluate");
|
|
494
|
+
expect(lastLocalClient).toBeDefined();
|
|
495
|
+
|
|
496
|
+
client.dispose();
|
|
497
|
+
expect(lastLocalClient?.dispose).toHaveBeenCalledTimes(1);
|
|
498
|
+
|
|
499
|
+
// A second dispose is a no-op.
|
|
500
|
+
client.dispose();
|
|
501
|
+
expect(lastLocalClient?.dispose).toHaveBeenCalledTimes(1);
|
|
502
|
+
|
|
503
|
+
await expect(client.send("Runtime.evaluate")).rejects.toMatchObject({
|
|
504
|
+
code: "disposed",
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("dispose() on an extension-backed client tears down the extension client", async () => {
|
|
509
|
+
const fakeProxy = makeAvailableProxy();
|
|
510
|
+
const ctx = makeContext({
|
|
511
|
+
conversationId: "dispose-ext",
|
|
512
|
+
hostBrowserProxy: fakeProxy,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const client = getCdpClient(ctx);
|
|
516
|
+
await client.send("Page.navigate");
|
|
517
|
+
client.dispose();
|
|
518
|
+
|
|
519
|
+
expect(lastExtensionClient?.dispose).toHaveBeenCalledTimes(1);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("dispose() on a cdp-inspect-backed client tears down the inspect client", async () => {
|
|
523
|
+
cdpInspectEnabled = true;
|
|
524
|
+
const ctx = makeContext({ conversationId: "dispose-inspect" });
|
|
525
|
+
|
|
526
|
+
const client = getCdpClient(ctx);
|
|
527
|
+
await client.send("Page.navigate");
|
|
528
|
+
client.dispose();
|
|
529
|
+
|
|
530
|
+
expect(lastCdpInspectClient?.dispose).toHaveBeenCalledTimes(1);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("send() after dispose() on a cdp-inspect-backed client rejects with disposed", async () => {
|
|
534
|
+
cdpInspectEnabled = true;
|
|
535
|
+
const ctx = makeContext({ conversationId: "post-dispose-inspect" });
|
|
536
|
+
|
|
537
|
+
const client = getCdpClient(ctx);
|
|
538
|
+
await client.send("Page.navigate");
|
|
539
|
+
client.dispose();
|
|
540
|
+
|
|
541
|
+
// Double dispose is a no-op.
|
|
542
|
+
client.dispose();
|
|
543
|
+
expect(lastCdpInspectClient?.dispose).toHaveBeenCalledTimes(1);
|
|
544
|
+
|
|
545
|
+
await expect(client.send("Page.navigate")).rejects.toMatchObject({
|
|
546
|
+
code: "disposed",
|
|
547
|
+
});
|
|
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
|
+
});
|
|
1993
|
+
});
|