@vellumai/assistant 0.9.0 → 0.10.0-staging.2
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 +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +28 -22
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +976 -63
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/access-request-card-view.test.ts +98 -0
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
- package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-compiler.test.ts +15 -1
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/cancel-clears-processing.test.ts +89 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -4
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
- package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
- package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
- package/src/__tests__/config-loader-backfill.test.ts +268 -27
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +0 -6
- package/src/__tests__/contacts-tools.test.ts +29 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +58 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-lifecycle.test.ts +7 -9
- package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
- package/src/__tests__/conversation-title-service.test.ts +62 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +4 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +4 -13
- package/src/__tests__/disk-pressure-policy.test.ts +12 -0
- package/src/__tests__/disk-usage.test.ts +65 -0
- package/src/__tests__/dynamic-page-surface.test.ts +152 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-flag-listener.test.ts +110 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
- package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +3 -35
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/headless-browser-mode.test.ts +10 -0
- package/src/__tests__/headless-browser-navigate.test.ts +8 -3
- package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
- package/src/__tests__/host-browser-proxy.test.ts +87 -0
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/injector-v3-suppression.test.ts +27 -20
- package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
- package/src/__tests__/invite-redemption-service.test.ts +4 -7
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +30 -23
- package/src/__tests__/llm-resolver.test.ts +70 -24
- package/src/__tests__/llm-schema.test.ts +1 -0
- package/src/__tests__/managed-profile-guard.test.ts +163 -4
- package/src/__tests__/mcp-health-check.test.ts +6 -7
- package/src/__tests__/media-stream-server-integration.test.ts +317 -13
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/path-policy.test.ts +34 -0
- package/src/__tests__/persona-resolver.test.ts +49 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/plugin-api-provider.test.ts +24 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
- package/src/__tests__/reaction-persistence.test.ts +150 -29
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/relay-server.test.ts +285 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
- package/src/__tests__/skill-execute-input.test.ts +51 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/skills.test.ts +51 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
- package/src/__tests__/subagent-tools.test.ts +266 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +100 -3
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
- package/src/__tests__/tool-audit-listener.test.ts +7 -7
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +8 -10
- package/src/__tests__/twilio-routes.test.ts +81 -1
- package/src/__tests__/voice-invite-redemption.test.ts +2 -3
- package/src/__tests__/weak-open-model.test.ts +30 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
- package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
- package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
- package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
- package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
- package/src/agent/loop.ts +49 -29
- package/src/api/README.md +6 -6
- package/src/api/events/tool-result.ts +6 -0
- package/src/api/events/workflow-completed.ts +53 -0
- package/src/api/events/workflow-leaf-finished.ts +38 -0
- package/src/api/events/workflow-leaf-started.ts +35 -0
- package/src/api/events/workflow-progress.ts +32 -0
- package/src/api/events/workflow-started.ts +31 -0
- package/src/api/index.ts +40 -0
- package/src/api/responses/conversation-message.ts +28 -4
- package/src/api/responses/home.ts +26 -4
- package/src/api/responses/workflow-journal.ts +53 -0
- package/src/approvals/guardian-card-withdrawal.ts +145 -0
- package/src/approvals/guardian-decision-primitive.ts +26 -3
- package/src/approvals/guardian-request-resolvers.ts +183 -80
- package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
- package/src/calls/call-pointer-messages.ts +10 -4
- package/src/calls/channel-admission-reader.ts +104 -0
- package/src/calls/guardian-dispatch.ts +17 -45
- package/src/calls/media-stream-server.ts +84 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/relay-server.ts +66 -0
- package/src/calls/relay-setup-router.ts +82 -1
- package/src/calls/twilio-routes.ts +17 -8
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/clients.ts +3 -0
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
- package/src/cli/commands/memory/index.ts +30 -0
- package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
- package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
- package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
- package/src/cli/commands/oauth/status.test.ts +36 -0
- package/src/cli/commands/oauth/status.ts +23 -3
- package/src/cli/commands/plugins.ts +197 -4
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/inspect-plugin.ts +12 -1
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +328 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/plugin-surfaces.ts +104 -0
- package/src/cli/lib/upgrade-plugin.ts +298 -10
- package/src/cli/program.ts +2 -6
- package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
- package/src/config/assistant-feature-flags.ts +22 -7
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +6 -4
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +14 -8
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +15 -2
- package/src/config/feature-flag-registry.json +46 -31
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/llm-resolver.ts +3 -0
- package/src/config/loader.ts +4 -0
- package/src/config/memory-v3-gate.ts +11 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schema.ts +8 -6
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/channels.ts +11 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/llm.ts +31 -0
- package/src/config/schemas/memory-lifecycle.ts +3 -7
- package/src/config/schemas/memory-v3.ts +6 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/services.ts +18 -0
- package/src/config/seed-inference-profiles.ts +109 -44
- package/src/config/skills.ts +21 -0
- package/src/config/sync-gated-profiles.ts +220 -0
- package/src/contacts/contact-store.ts +89 -106
- package/src/contacts/contacts-write.ts +5 -22
- package/src/contacts/types.ts +0 -1
- package/src/context/compactor.ts +88 -54
- package/src/context/strip-injections.ts +58 -10
- package/src/context/token-estimator.ts +1 -1
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
- package/src/daemon/conversation-agent-loop.ts +114 -22
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +3 -5
- package/src/daemon/conversation-process.ts +13 -5
- package/src/daemon/conversation-runtime-assembly.ts +13 -15
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-surfaces.ts +26 -0
- package/src/daemon/conversation-tool-setup.ts +27 -14
- package/src/daemon/conversation.ts +66 -14
- package/src/daemon/disk-pressure-policy.ts +5 -3
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
- package/src/daemon/handlers/config-a2a.ts +0 -2
- package/src/daemon/handlers/config-channels.ts +15 -16
- package/src/daemon/handlers/config-slack-channel.ts +22 -3
- package/src/daemon/handlers/conversations.ts +107 -0
- package/src/daemon/host-browser-proxy.ts +41 -0
- package/src/daemon/lifecycle.ts +55 -27
- package/src/daemon/message-provenance.ts +2 -0
- package/src/daemon/message-types/contacts.ts +0 -1
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/message-types/web-activity.ts +7 -1
- package/src/daemon/message-types/workflows.ts +83 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/daemon/tool-setup-types.ts +4 -0
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -2
- package/src/home/feed-source-enrichment.test.ts +151 -0
- package/src/home/feed-source-enrichment.ts +176 -0
- package/src/home/relationship-state.ts +2 -4
- package/src/instrument.ts +18 -6
- package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
- package/src/ipc/assistant-server.ts +37 -4
- package/src/ipc/gateway-flag-listener.ts +18 -2
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
- package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
- package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
- package/src/memory/auth-fallback-events-store.ts +2 -2
- package/src/memory/auto-analysis-enqueue.ts +3 -5
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/canonical-guardian-store.ts +39 -1
- package/src/memory/conversation-crud.ts +9 -4
- package/src/memory/conversation-key-store.ts +17 -2
- package/src/memory/conversation-title-service.ts +64 -7
- package/src/memory/db-init.ts +17 -17
- package/src/memory/embedding-backend.ts +38 -1
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +54 -1
- package/src/memory/lifecycle-events-store.ts +2 -2
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-enqueue.ts +31 -6
- package/src/memory/memory-retrospective-job.ts +28 -227
- package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
- package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
- package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
- package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
- package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/onboarding-events-store.ts +3 -3
- package/src/memory/schema/contacts.ts +0 -5
- package/src/memory/skill-loaded-events-store.test.ts +7 -15
- package/src/memory/skill-loaded-events-store.ts +2 -2
- package/src/memory/tool-executed-events-store.test.ts +7 -7
- package/src/memory/turn-trace-store.test.ts +736 -0
- package/src/memory/turn-trace-store.ts +364 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
- package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
- package/src/memory/v2/consolidation-job.ts +2 -2
- package/src/memory/v2/skill-content.ts +25 -7
- package/src/memory/v2/skill-store.ts +7 -1
- package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
- package/src/memory/v3-eval/eval-packets.ts +546 -0
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/messaging/providers/slack/api.ts +31 -0
- package/src/messaging/providers/slack/send.test.ts +114 -2
- package/src/messaging/providers/slack/send.ts +30 -7
- package/src/messaging/providers/slack/withdraw.test.ts +200 -0
- package/src/messaging/providers/slack/withdraw.ts +161 -0
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/access-request-copy.ts +72 -59
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +58 -103
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/approval-card-data.ts +333 -0
- package/src/notifications/broadcaster.ts +16 -3
- package/src/notifications/canonical-delivery-recorder.ts +139 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +4 -2
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/guardian-question-mode.ts +10 -0
- package/src/notifications/home-feed-side-effect.ts +7 -16
- package/src/notifications/notification-utils.ts +19 -20
- package/src/notifications/signal.ts +79 -43
- package/src/notifications/types.ts +98 -121
- package/src/oauth/AGENTS.md +5 -24
- package/src/permissions/checker.test.ts +51 -0
- package/src/permissions/checker.ts +185 -26
- package/src/permissions/ipc-risk-types.ts +24 -0
- package/src/permissions/question-prompter.test.ts +27 -0
- package/src/permissions/question-prompter.ts +4 -0
- package/src/platform/client.test.ts +119 -0
- package/src/platform/client.ts +66 -0
- package/src/platform/consent-cache.test.ts +267 -0
- package/src/platform/consent-cache.ts +174 -0
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +33 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
- package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
- package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
- package/src/plugins/defaults/advisor/config.ts +21 -0
- package/src/plugins/defaults/advisor/consult.ts +93 -0
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/advisor/package.json +14 -0
- package/src/plugins/defaults/advisor/steering.ts +67 -0
- package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
- package/src/plugins/defaults/advisor/transcript.ts +76 -0
- package/src/plugins/defaults/index.ts +60 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
- package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
- package/src/prompts/persona-resolver.ts +14 -4
- package/src/prompts/templates/system-sections.ts +7 -2
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +176 -0
- package/src/providers/atlascloud/client.ts +85 -0
- package/src/providers/fetch-provider-catalog.ts +85 -0
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/model-catalog.ts +58 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
- package/src/providers/openai/chat-completions-provider.ts +7 -0
- package/src/providers/openai/responses-provider.ts +10 -0
- package/src/providers/provider-send-message.ts +11 -3
- package/src/providers/retry.ts +53 -12
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/weak-open-model.ts +22 -0
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/__tests__/client-health.test.ts +44 -0
- package/src/runtime/access-request-helper.ts +21 -53
- package/src/runtime/actor-trust-resolver.ts +59 -63
- package/src/runtime/agent-wake.ts +52 -0
- package/src/runtime/assistant-event-hub.ts +18 -4
- package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/capabilities.test.ts +120 -0
- package/src/runtime/capabilities.ts +197 -0
- package/src/runtime/channel-approval-types.ts +22 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-retry-sweep.ts +1 -0
- package/src/runtime/channel-verification-service.ts +3 -3
- package/src/runtime/client-health.ts +26 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
- package/src/runtime/effective-capabilities.test.ts +128 -0
- package/src/runtime/effective-capabilities.ts +84 -0
- package/src/runtime/guardian-reply-router.ts +106 -21
- package/src/runtime/invite-redemption-service.ts +9 -25
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
- package/src/runtime/migrations/vbundle-builder.ts +49 -20
- package/src/runtime/pending-interactions.ts +15 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/browser-tabs-routes.ts +9 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
- package/src/runtime/routes/client-routes.ts +10 -0
- package/src/runtime/routes/contact-routes.ts +31 -8
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +80 -1
- package/src/runtime/routes/conversation-query-routes.ts +68 -22
- package/src/runtime/routes/conversation-routes.ts +39 -14
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/guardian-approval-interception.ts +14 -73
- package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
- package/src/runtime/routes/home-feed-routes.ts +8 -3
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-message-handler.ts +214 -228
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
- package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
- package/src/runtime/routes/integrations/slack/channel.ts +36 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
- package/src/runtime/routes/mcp-auth-routes.ts +233 -41
- package/src/runtime/routes/memory-eval-routes.ts +87 -0
- package/src/runtime/routes/notification-routes.ts +122 -133
- package/src/runtime/routes/platform-routes.ts +2 -2
- package/src/runtime/routes/plugins-routes.ts +202 -3
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/surface-action-routes.ts +2 -1
- package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
- package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
- package/src/runtime/routes/workflow-routes.test.ts +229 -44
- package/src/runtime/routes/workflow-routes.ts +131 -29
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/runtime/tool-grant-request-helper.ts +18 -16
- package/src/runtime/trust-context-resolver.ts +8 -5
- package/src/schedule/inference-profile.ts +2 -14
- package/src/schedule/schedule-store.ts +1 -1
- package/src/schedule/scheduler-types.ts +5 -1
- package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/subagent/manager.ts +17 -4
- package/src/subagent/types.ts +6 -0
- package/src/telemetry/trace-collection-policy.test.ts +28 -0
- package/src/telemetry/trace-collection-policy.ts +30 -0
- package/src/telemetry/types.ts +89 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
- package/src/telemetry/usage-telemetry-reporter.ts +148 -41
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
- package/src/tools/browser/browser-execution.ts +30 -19
- package/src/tools/document/document-tool.ts +2 -3
- package/src/tools/executor.ts +5 -3
- package/src/tools/host-terminal/host-shell.ts +5 -4
- package/src/tools/memory/register.ts +2 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
- package/src/tools/network/__tests__/web-search.test.ts +143 -0
- package/src/tools/network/web-fetch.ts +372 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/network/web-search.ts +213 -10
- package/src/tools/permission-checker.ts +4 -3
- package/src/tools/registry.ts +20 -0
- package/src/tools/schedule/create.ts +7 -12
- package/src/tools/schedule/update.ts +4 -11
- package/src/tools/shared/filesystem/path-policy.ts +39 -13
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +33 -0
- package/src/tools/subagent/spawn.ts +61 -12
- package/src/tools/terminal/shell.ts +10 -4
- package/src/tools/tool-approval-handler.ts +18 -13
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/types.ts +9 -0
- package/src/tools/ui-surface/definitions.ts +64 -3
- package/src/tools/verification-control-plane-policy.ts +3 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/tools/workflows/run-workflow.ts +1 -0
- package/src/util/disk-usage.ts +78 -23
- package/src/util/platform.ts +10 -3
- package/src/watcher/telemetry.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/engine.test.ts +175 -1
- package/src/workflows/engine.ts +82 -0
- package/src/workflows/journal-store.test.ts +70 -0
- package/src/workflows/journal-store.ts +18 -3
- package/src/workflows/run-manager.test.ts +171 -28
- package/src/workflows/run-manager.ts +66 -24
- package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
- package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
- package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
- package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/notifications/tool-approval-copy.ts +0 -142
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - `user-prompt-submit` — first-pass generation from the submitted prompt,
|
|
8
8
|
* scheduled on a later macrotask so the main agent-loop LLM request is
|
|
9
9
|
* issued first.
|
|
10
|
-
* - `stop` —
|
|
11
|
-
*
|
|
10
|
+
* - `stop` — fallback retry for replaceable titles and second-pass
|
|
11
|
+
* regeneration once the conversation reaches its third user turn.
|
|
12
12
|
*
|
|
13
13
|
* Both let the title service resolve the provider, persist the title, and
|
|
14
14
|
* broadcast the resulting `conversation_title_updated` / `sync_changed`
|
|
@@ -31,13 +31,42 @@ const queueGenerateConversationTitleMock = mock(
|
|
|
31
31
|
}): void => undefined,
|
|
32
32
|
);
|
|
33
33
|
const queueRegenerateConversationTitleMock = mock(
|
|
34
|
-
(_params: {
|
|
34
|
+
(_params: {
|
|
35
|
+
conversationId: string;
|
|
36
|
+
provider?: unknown;
|
|
37
|
+
onlyIfReplaceable?: boolean;
|
|
38
|
+
}): void => undefined,
|
|
35
39
|
);
|
|
36
40
|
mock.module("../memory/conversation-title-service.js", () => ({
|
|
41
|
+
AUTO_TITLE_DETERMINISTIC: 2,
|
|
42
|
+
isReplaceableTitle: (title: string | null) =>
|
|
43
|
+
title == null ||
|
|
44
|
+
title === "" ||
|
|
45
|
+
title === "Generating title..." ||
|
|
46
|
+
title === "New Conversation" ||
|
|
47
|
+
title === "Untitled" ||
|
|
48
|
+
title === "Untitled Conversation" ||
|
|
49
|
+
title.startsWith("Runtime: "),
|
|
37
50
|
queueGenerateConversationTitle: queueGenerateConversationTitleMock,
|
|
38
51
|
queueRegenerateConversationTitle: queueRegenerateConversationTitleMock,
|
|
39
52
|
}));
|
|
40
53
|
|
|
54
|
+
const mockGetConversation = mock(
|
|
55
|
+
(_conversationId: string) =>
|
|
56
|
+
({
|
|
57
|
+
title: "Existing Title",
|
|
58
|
+
isAutoTitle: 1,
|
|
59
|
+
conversationType: "standard",
|
|
60
|
+
}) as {
|
|
61
|
+
title: string;
|
|
62
|
+
isAutoTitle: number;
|
|
63
|
+
conversationType: string;
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
67
|
+
getConversation: mockGetConversation,
|
|
68
|
+
}));
|
|
69
|
+
|
|
41
70
|
// The `stop` hook reads `conversations.skipAutoRetitling`; stub the loader so
|
|
42
71
|
// the opt-out is controllable per test.
|
|
43
72
|
let skipAutoRetitling = false;
|
|
@@ -192,6 +221,19 @@ describe("title-generate stop hook", () => {
|
|
|
192
221
|
resetPluginRegistryForTests();
|
|
193
222
|
queueRegenerateConversationTitleMock.mockReset();
|
|
194
223
|
queueRegenerateConversationTitleMock.mockImplementation(() => undefined);
|
|
224
|
+
mockGetConversation.mockReset();
|
|
225
|
+
mockGetConversation.mockImplementation(
|
|
226
|
+
(_conversationId: string) =>
|
|
227
|
+
({
|
|
228
|
+
title: "Existing Title",
|
|
229
|
+
isAutoTitle: 1,
|
|
230
|
+
conversationType: "standard",
|
|
231
|
+
}) as {
|
|
232
|
+
title: string;
|
|
233
|
+
isAutoTitle: number;
|
|
234
|
+
conversationType: string;
|
|
235
|
+
},
|
|
236
|
+
);
|
|
195
237
|
skipAutoRetitling = false;
|
|
196
238
|
});
|
|
197
239
|
|
|
@@ -210,6 +252,61 @@ describe("title-generate stop hook", () => {
|
|
|
210
252
|
expect(call?.conversationId).toBe("conv-1");
|
|
211
253
|
expect(call).not.toHaveProperty("provider");
|
|
212
254
|
expect(call).not.toHaveProperty("signal");
|
|
255
|
+
expect(call).not.toHaveProperty("onlyIfReplaceable");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("retries a replaceable fallback title after a successful turn", async () => {
|
|
259
|
+
mockGetConversation.mockReturnValueOnce({
|
|
260
|
+
title: "Untitled Conversation",
|
|
261
|
+
isAutoTitle: 2,
|
|
262
|
+
conversationType: "standard",
|
|
263
|
+
});
|
|
264
|
+
const ctx = makeStopCtx({ messages: historyWithUserTurns(1) });
|
|
265
|
+
|
|
266
|
+
await stop(ctx);
|
|
267
|
+
await flushMacrotasks();
|
|
268
|
+
|
|
269
|
+
expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
|
|
270
|
+
expect(queueRegenerateConversationTitleMock).toHaveBeenCalledWith({
|
|
271
|
+
conversationId: "conv-1",
|
|
272
|
+
onlyIfReplaceable: true,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("preserves the third-turn retitle when the title is still replaceable", async () => {
|
|
277
|
+
mockGetConversation.mockReturnValueOnce({
|
|
278
|
+
title: "Untitled Conversation",
|
|
279
|
+
isAutoTitle: 2,
|
|
280
|
+
conversationType: "standard",
|
|
281
|
+
});
|
|
282
|
+
const ctx = makeStopCtx({ messages: historyWithUserTurns(3) });
|
|
283
|
+
|
|
284
|
+
await stop(ctx);
|
|
285
|
+
await flushMacrotasks();
|
|
286
|
+
|
|
287
|
+
expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
|
|
288
|
+
const call = queueRegenerateConversationTitleMock.mock.calls[0]?.[0];
|
|
289
|
+
expect(call?.conversationId).toBe("conv-1");
|
|
290
|
+
expect(call).not.toHaveProperty("onlyIfReplaceable");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("fallback title retry is not blocked by the retitling opt-out", async () => {
|
|
294
|
+
skipAutoRetitling = true;
|
|
295
|
+
mockGetConversation.mockReturnValueOnce({
|
|
296
|
+
title: "Generating title...",
|
|
297
|
+
isAutoTitle: 1,
|
|
298
|
+
conversationType: "standard",
|
|
299
|
+
});
|
|
300
|
+
const ctx = makeStopCtx({ messages: historyWithUserTurns(1) });
|
|
301
|
+
|
|
302
|
+
await stop(ctx);
|
|
303
|
+
await flushMacrotasks();
|
|
304
|
+
|
|
305
|
+
expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
|
|
306
|
+
expect(queueRegenerateConversationTitleMock).toHaveBeenCalledWith({
|
|
307
|
+
conversationId: "conv-1",
|
|
308
|
+
onlyIfReplaceable: true,
|
|
309
|
+
});
|
|
213
310
|
});
|
|
214
311
|
|
|
215
312
|
test("defers the regeneration so the completed turn is persisted first", async () => {
|
|
@@ -128,7 +128,6 @@ function makeSystemPrompt(size: "small" | "production" = "small"): string {
|
|
|
128
128
|
"- `file_write` — Creating or overwriting files",
|
|
129
129
|
"- `file_edit` — Modifying existing files",
|
|
130
130
|
"- `file_delete` — Deleting files",
|
|
131
|
-
"- `credential_store set` — Storing credentials",
|
|
132
131
|
"",
|
|
133
132
|
"### Medium-Risk Tools (require approval on first use per session)",
|
|
134
133
|
"- `web_fetch` — Fetching external URLs",
|
|
@@ -349,7 +348,7 @@ function makeToolDefinitions(): Array<{
|
|
|
349
348
|
input_schema: object;
|
|
350
349
|
}> = [];
|
|
351
350
|
|
|
352
|
-
// Core tools (
|
|
351
|
+
// Core tools (10)
|
|
353
352
|
tools.push(
|
|
354
353
|
{
|
|
355
354
|
name: "bash",
|
|
@@ -566,33 +565,6 @@ function makeToolDefinitions(): Array<{
|
|
|
566
565
|
required: ["query"],
|
|
567
566
|
},
|
|
568
567
|
},
|
|
569
|
-
{
|
|
570
|
-
name: "credential_store",
|
|
571
|
-
description:
|
|
572
|
-
"Securely store or retrieve credentials for external services. Credentials are encrypted at rest.",
|
|
573
|
-
input_schema: {
|
|
574
|
-
type: "object",
|
|
575
|
-
properties: {
|
|
576
|
-
action: {
|
|
577
|
-
type: "string",
|
|
578
|
-
enum: ["get", "set", "delete", "list"],
|
|
579
|
-
},
|
|
580
|
-
service: {
|
|
581
|
-
type: "string",
|
|
582
|
-
description: "The service name (e.g., 'github', 'slack')",
|
|
583
|
-
},
|
|
584
|
-
key: {
|
|
585
|
-
type: "string",
|
|
586
|
-
description: "Credential key within the service",
|
|
587
|
-
},
|
|
588
|
-
value: {
|
|
589
|
-
type: "string",
|
|
590
|
-
description: "Credential value (required for 'set')",
|
|
591
|
-
},
|
|
592
|
-
},
|
|
593
|
-
required: ["action", "service"],
|
|
594
|
-
},
|
|
595
|
-
},
|
|
596
568
|
);
|
|
597
569
|
|
|
598
570
|
// Computer-use proxy tools (11)
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeAll,
|
|
9
|
+
beforeEach,
|
|
10
|
+
describe,
|
|
11
|
+
expect,
|
|
12
|
+
mock,
|
|
13
|
+
test,
|
|
14
|
+
} from "bun:test";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Mock logger
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
mock.module("../util/logger.js", () => ({
|
|
21
|
+
getLogger: () =>
|
|
22
|
+
new Proxy({} as Record<string, unknown>, {
|
|
23
|
+
get: () => () => {},
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Use encrypted backend with a temp store path
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
import { _resetBackend, setSecureKeyAsync } from "../security/secure-keys.js";
|
|
32
|
+
import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
|
|
33
|
+
|
|
34
|
+
const TEST_DIR = join(
|
|
35
|
+
tmpdir(),
|
|
36
|
+
`vellum-token-manager-test-${randomBytes(4).toString("hex")}`,
|
|
37
|
+
);
|
|
38
|
+
const STORE_PATH = join(TEST_DIR, "keys.enc");
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Mock OAuth2 token refresh so dedup can be observed without network I/O
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
let mockRefreshOAuth2Token: ReturnType<
|
|
45
|
+
typeof mock<
|
|
46
|
+
(
|
|
47
|
+
tokenExchangeUrl: string,
|
|
48
|
+
clientId: string,
|
|
49
|
+
refreshToken: string,
|
|
50
|
+
clientSecret?: string,
|
|
51
|
+
tokenEndpointAuthMethod?: string,
|
|
52
|
+
) => Promise<{ accessToken: string; expiresIn: number }>
|
|
53
|
+
>
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
mock.module("../security/oauth2.js", () => {
|
|
57
|
+
mockRefreshOAuth2Token = mock(() =>
|
|
58
|
+
Promise.resolve({
|
|
59
|
+
accessToken: "refreshed-access-token",
|
|
60
|
+
expiresIn: 3600,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
refreshOAuth2Token: mockRefreshOAuth2Token,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Mock oauth-store — token-manager reads refresh config from SQLite
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/** Mutable per-test map of provider connections for getConnectionByProvider */
|
|
73
|
+
const mockConnections = new Map<
|
|
74
|
+
string,
|
|
75
|
+
{
|
|
76
|
+
id: string;
|
|
77
|
+
provider: string;
|
|
78
|
+
oauthAppId: string;
|
|
79
|
+
expiresAt: number | null;
|
|
80
|
+
}
|
|
81
|
+
>();
|
|
82
|
+
const mockApps = new Map<
|
|
83
|
+
string,
|
|
84
|
+
{
|
|
85
|
+
id: string;
|
|
86
|
+
provider: string;
|
|
87
|
+
clientId: string;
|
|
88
|
+
clientSecretCredentialPath: string;
|
|
89
|
+
}
|
|
90
|
+
>();
|
|
91
|
+
const mockProviders = new Map<
|
|
92
|
+
string,
|
|
93
|
+
{
|
|
94
|
+
key: string;
|
|
95
|
+
tokenExchangeUrl: string;
|
|
96
|
+
refreshUrl?: string | null;
|
|
97
|
+
tokenEndpointAuthMethod?: string;
|
|
98
|
+
}
|
|
99
|
+
>();
|
|
100
|
+
|
|
101
|
+
mock.module("../oauth/oauth-store.js", () => ({
|
|
102
|
+
getConnectionByProvider: (service: string) => mockConnections.get(service),
|
|
103
|
+
getConnection: (id: string) => {
|
|
104
|
+
for (const conn of mockConnections.values()) {
|
|
105
|
+
if (conn.id === id) return conn;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
},
|
|
109
|
+
getApp: (id: string) => mockApps.get(id),
|
|
110
|
+
getProvider: (key: string) => mockProviders.get(key),
|
|
111
|
+
updateConnection: () => {},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Import the modules under test
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
import {
|
|
119
|
+
_resetInflightRefreshes,
|
|
120
|
+
_resetRefreshBreakers,
|
|
121
|
+
withValidToken,
|
|
122
|
+
} from "../security/token-manager.js";
|
|
123
|
+
import { _setMetadataPath } from "../tools/credentials/metadata-store.js";
|
|
124
|
+
|
|
125
|
+
describe("withValidToken refresh deduplication", () => {
|
|
126
|
+
beforeAll(() => {
|
|
127
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
_resetBackend();
|
|
132
|
+
for (const entry of readdirSync(TEST_DIR)) {
|
|
133
|
+
rmSync(join(TEST_DIR, entry), { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
setStorePathForTesting(STORE_PATH);
|
|
136
|
+
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
137
|
+
_resetRefreshBreakers();
|
|
138
|
+
_resetInflightRefreshes();
|
|
139
|
+
mockRefreshOAuth2Token.mockClear();
|
|
140
|
+
// Clear mock oauth-store maps
|
|
141
|
+
mockConnections.clear();
|
|
142
|
+
mockApps.clear();
|
|
143
|
+
mockProviders.clear();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
_setMetadataPath(null);
|
|
148
|
+
setStorePathForTesting(null);
|
|
149
|
+
_resetBackend();
|
|
150
|
+
_resetRefreshBreakers();
|
|
151
|
+
_resetInflightRefreshes();
|
|
152
|
+
mockConnections.clear();
|
|
153
|
+
mockApps.clear();
|
|
154
|
+
mockProviders.clear();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterAll(() => {
|
|
158
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Helper: set up a service with an access token, refresh token, and
|
|
163
|
+
* mock DB data so that token refresh can proceed through doRefresh().
|
|
164
|
+
*
|
|
165
|
+
* OAuth-specific fields (tokenExchangeUrl, clientId, expiresAt) are stored
|
|
166
|
+
* in the SQLite oauth-store. The mock maps simulate the DB layer.
|
|
167
|
+
*/
|
|
168
|
+
async function setupService(
|
|
169
|
+
service: string,
|
|
170
|
+
opts?: { expired?: boolean; accessToken?: string },
|
|
171
|
+
) {
|
|
172
|
+
const accessToken = opts?.accessToken ?? "old-access-token";
|
|
173
|
+
|
|
174
|
+
// Seed mock oauth-store maps so token-manager can resolve refresh config
|
|
175
|
+
const appId = `app-${service}`;
|
|
176
|
+
const connId = `conn-${service}`;
|
|
177
|
+
|
|
178
|
+
// Store access token under the oauth_connection key path that
|
|
179
|
+
// withValidToken reads (not the legacy credentialKey path).
|
|
180
|
+
await setSecureKeyAsync(
|
|
181
|
+
`oauth_connection/${connId}/access_token`,
|
|
182
|
+
accessToken,
|
|
183
|
+
);
|
|
184
|
+
mockProviders.set(service, {
|
|
185
|
+
key: service,
|
|
186
|
+
tokenExchangeUrl: "https://oauth.example.com/token",
|
|
187
|
+
refreshUrl: null,
|
|
188
|
+
});
|
|
189
|
+
mockApps.set(appId, {
|
|
190
|
+
id: appId,
|
|
191
|
+
provider: service,
|
|
192
|
+
clientId: "test-client-id",
|
|
193
|
+
clientSecretCredentialPath: `oauth_app/${appId}/client_secret`,
|
|
194
|
+
});
|
|
195
|
+
mockConnections.set(service, {
|
|
196
|
+
id: connId,
|
|
197
|
+
provider: service,
|
|
198
|
+
oauthAppId: appId,
|
|
199
|
+
expiresAt: opts?.expired
|
|
200
|
+
? Date.now() - 60_000 // expired 1 minute ago
|
|
201
|
+
: Date.now() + 3600_000, // expires in 1 hour
|
|
202
|
+
});
|
|
203
|
+
// Store refresh token and client_secret in secure keys (token-manager reads them)
|
|
204
|
+
await setSecureKeyAsync(
|
|
205
|
+
`oauth_connection/${connId}/refresh_token`,
|
|
206
|
+
"valid-refresh-token",
|
|
207
|
+
);
|
|
208
|
+
await setSecureKeyAsync(
|
|
209
|
+
`oauth_app/${appId}/client_secret`,
|
|
210
|
+
"test-client-secret",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
|
|
215
|
+
await setupService("google");
|
|
216
|
+
|
|
217
|
+
let resolveRefresh!: (value: {
|
|
218
|
+
accessToken: string;
|
|
219
|
+
expiresIn: number;
|
|
220
|
+
}) => void;
|
|
221
|
+
const refreshPromise = new Promise<{
|
|
222
|
+
accessToken: string;
|
|
223
|
+
expiresIn: number;
|
|
224
|
+
}>((resolve) => {
|
|
225
|
+
resolveRefresh = resolve;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
mockRefreshOAuth2Token.mockImplementation(() => refreshPromise);
|
|
229
|
+
|
|
230
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
231
|
+
|
|
232
|
+
const callback = async (token: string) => {
|
|
233
|
+
if (token === "old-access-token") throw err401;
|
|
234
|
+
return `result-with-${token}`;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Launch 3 concurrent withValidToken calls — all will get a non-expired
|
|
238
|
+
// token first, call the callback, get a 401, and then try to refresh.
|
|
239
|
+
const p1 = withValidToken("google", callback);
|
|
240
|
+
const p2 = withValidToken("google", callback);
|
|
241
|
+
const p3 = withValidToken("google", callback);
|
|
242
|
+
|
|
243
|
+
// Let the event loop tick so all 3 calls enter the 401 retry path
|
|
244
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
245
|
+
|
|
246
|
+
// Resolve the single refresh attempt
|
|
247
|
+
resolveRefresh({ accessToken: "new-token-123", expiresIn: 3600 });
|
|
248
|
+
|
|
249
|
+
const results = await Promise.all([p1, p2, p3]);
|
|
250
|
+
|
|
251
|
+
// All 3 should succeed with the refreshed token
|
|
252
|
+
expect(results).toEqual([
|
|
253
|
+
"result-with-new-token-123",
|
|
254
|
+
"result-with-new-token-123",
|
|
255
|
+
"result-with-new-token-123",
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
// refreshOAuth2Token should have been called exactly once
|
|
259
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("concurrent refreshes for different services proceed independently", async () => {
|
|
263
|
+
await setupService("google");
|
|
264
|
+
await setupService("slack");
|
|
265
|
+
|
|
266
|
+
let resolveGmail!: (value: {
|
|
267
|
+
accessToken: string;
|
|
268
|
+
expiresIn: number;
|
|
269
|
+
}) => void;
|
|
270
|
+
let resolveSlack!: (value: {
|
|
271
|
+
accessToken: string;
|
|
272
|
+
expiresIn: number;
|
|
273
|
+
}) => void;
|
|
274
|
+
|
|
275
|
+
const gmailPromise = new Promise<{
|
|
276
|
+
accessToken: string;
|
|
277
|
+
expiresIn: number;
|
|
278
|
+
}>((resolve) => {
|
|
279
|
+
resolveGmail = resolve;
|
|
280
|
+
});
|
|
281
|
+
const slackPromise = new Promise<{
|
|
282
|
+
accessToken: string;
|
|
283
|
+
expiresIn: number;
|
|
284
|
+
}>((resolve) => {
|
|
285
|
+
resolveSlack = resolve;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
let refreshCallCount = 0;
|
|
289
|
+
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
290
|
+
refreshCallCount++;
|
|
291
|
+
// Both services use the same tokenExchangeUrl in this test, so we track by
|
|
292
|
+
// call order to return the correct deferred promise.
|
|
293
|
+
if (refreshCallCount === 1) return gmailPromise;
|
|
294
|
+
return slackPromise;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
298
|
+
|
|
299
|
+
const gmailCallback = async (token: string) => {
|
|
300
|
+
if (token === "old-access-token") throw err401;
|
|
301
|
+
return `gmail-${token}`;
|
|
302
|
+
};
|
|
303
|
+
const slackCallback = async (token: string) => {
|
|
304
|
+
if (token === "old-access-token") throw err401;
|
|
305
|
+
return `slack-${token}`;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const p1 = withValidToken("google", gmailCallback);
|
|
309
|
+
const p2 = withValidToken("slack", slackCallback);
|
|
310
|
+
|
|
311
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
312
|
+
|
|
313
|
+
// Resolve both independently
|
|
314
|
+
resolveGmail({ accessToken: "gmail-new-token", expiresIn: 3600 });
|
|
315
|
+
resolveSlack({ accessToken: "slack-new-token", expiresIn: 3600 });
|
|
316
|
+
|
|
317
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
318
|
+
|
|
319
|
+
expect(r1).toBe("gmail-gmail-new-token");
|
|
320
|
+
expect(r2).toBe("slack-slack-new-token");
|
|
321
|
+
|
|
322
|
+
// Both services should have triggered their own refresh
|
|
323
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(2);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
|
|
327
|
+
await setupService("google");
|
|
328
|
+
|
|
329
|
+
let refreshCount = 0;
|
|
330
|
+
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
331
|
+
refreshCount++;
|
|
332
|
+
return Promise.resolve({
|
|
333
|
+
accessToken: `token-${refreshCount}`,
|
|
334
|
+
expiresIn: 3600,
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
339
|
+
|
|
340
|
+
// First call triggers a refresh (old token → 401 → refresh → token-1)
|
|
341
|
+
const r1 = await withValidToken("google", async (token: string) => {
|
|
342
|
+
if (token !== "token-1") throw err401;
|
|
343
|
+
return token;
|
|
344
|
+
});
|
|
345
|
+
expect(r1).toBe("token-1");
|
|
346
|
+
expect(refreshCount).toBe(1);
|
|
347
|
+
|
|
348
|
+
// Second call also triggers a 401 to verify dedup state was cleaned up
|
|
349
|
+
// and a new refresh is allowed (not deduplicated with the first).
|
|
350
|
+
const r2 = await withValidToken("google", async (token: string) => {
|
|
351
|
+
if (token !== "token-2") throw err401;
|
|
352
|
+
return token;
|
|
353
|
+
});
|
|
354
|
+
expect(r2).toBe("token-2");
|
|
355
|
+
// Second refresh should have happened (not deduplicated with the first,
|
|
356
|
+
// since the first already completed)
|
|
357
|
+
expect(refreshCount).toBe(2);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("deduplication propagates refresh errors to all waiting callers", async () => {
|
|
361
|
+
await setupService("google");
|
|
362
|
+
|
|
363
|
+
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
364
|
+
Promise.reject(
|
|
365
|
+
Object.assign(
|
|
366
|
+
new Error("OAuth2 token refresh failed (HTTP 401: invalid_grant)"),
|
|
367
|
+
),
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
372
|
+
|
|
373
|
+
const callback = async (token: string) => {
|
|
374
|
+
if (token === "old-access-token") throw err401;
|
|
375
|
+
return "should-not-reach";
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Launch 2 concurrent calls — both should fail with the same error
|
|
379
|
+
const p1 = withValidToken("google", callback);
|
|
380
|
+
const p2 = withValidToken("google", callback);
|
|
381
|
+
|
|
382
|
+
const results = await Promise.allSettled([p1, p2]);
|
|
383
|
+
|
|
384
|
+
expect(results[0].status).toBe("rejected");
|
|
385
|
+
expect(results[1].status).toBe("rejected");
|
|
386
|
+
|
|
387
|
+
// Only one actual refresh attempt
|
|
388
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// -----------------------------------------------------------------------
|
|
392
|
+
// refreshUrl resolution — provider.refreshUrl with fallback to tokenExchangeUrl
|
|
393
|
+
// -----------------------------------------------------------------------
|
|
394
|
+
describe("refreshUrl resolution", () => {
|
|
395
|
+
test("uses provider.refreshUrl when set", async () => {
|
|
396
|
+
await setupService("google");
|
|
397
|
+
mockProviders.get("google")!.refreshUrl =
|
|
398
|
+
"https://refresh.example.com/token";
|
|
399
|
+
|
|
400
|
+
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
401
|
+
Promise.resolve({
|
|
402
|
+
accessToken: "new-token-from-refresh-url",
|
|
403
|
+
expiresIn: 3600,
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
408
|
+
|
|
409
|
+
const callback = async (token: string) => {
|
|
410
|
+
if (token === "old-access-token") throw err401;
|
|
411
|
+
return `result-with-${token}`;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const result = await withValidToken("google", callback);
|
|
415
|
+
|
|
416
|
+
expect(result).toBe("result-with-new-token-from-refresh-url");
|
|
417
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
418
|
+
// Assert the refresh endpoint passed in is provider.refreshUrl, not
|
|
419
|
+
// the tokenExchangeUrl fallback.
|
|
420
|
+
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
421
|
+
"https://refresh.example.com/token",
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("falls back to provider.tokenExchangeUrl when refreshUrl is null", async () => {
|
|
426
|
+
// setupService sets refreshUrl: null by default — this exercises the
|
|
427
|
+
// fallback path explicitly.
|
|
428
|
+
await setupService("google");
|
|
429
|
+
expect(mockProviders.get("google")!.refreshUrl).toBeNull();
|
|
430
|
+
|
|
431
|
+
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
432
|
+
Promise.resolve({
|
|
433
|
+
accessToken: "new-token-from-token-exchange-url",
|
|
434
|
+
expiresIn: 3600,
|
|
435
|
+
}),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
439
|
+
|
|
440
|
+
const callback = async (token: string) => {
|
|
441
|
+
if (token === "old-access-token") throw err401;
|
|
442
|
+
return `result-with-${token}`;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const result = await withValidToken("google", callback);
|
|
446
|
+
|
|
447
|
+
expect(result).toBe("result-with-new-token-from-token-exchange-url");
|
|
448
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
449
|
+
// Assert the refresh endpoint falls back to tokenExchangeUrl.
|
|
450
|
+
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
451
|
+
"https://oauth.example.com/token",
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("falls back to provider.tokenExchangeUrl when refreshUrl is undefined", async () => {
|
|
456
|
+
await setupService("google");
|
|
457
|
+
// Delete the refreshUrl field entirely so the property is `undefined`
|
|
458
|
+
// rather than `null`. Both representations of "not set" must produce
|
|
459
|
+
// the fallback behavior.
|
|
460
|
+
delete mockProviders.get("google")!.refreshUrl;
|
|
461
|
+
expect(mockProviders.get("google")!.refreshUrl).toBeUndefined();
|
|
462
|
+
|
|
463
|
+
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
464
|
+
Promise.resolve({
|
|
465
|
+
accessToken: "new-token-from-token-exchange-url",
|
|
466
|
+
expiresIn: 3600,
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
471
|
+
|
|
472
|
+
const callback = async (token: string) => {
|
|
473
|
+
if (token === "old-access-token") throw err401;
|
|
474
|
+
return `result-with-${token}`;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const result = await withValidToken("google", callback);
|
|
478
|
+
|
|
479
|
+
expect(result).toBe("result-with-new-token-from-token-exchange-url");
|
|
480
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
481
|
+
// Assert the refresh endpoint falls back to tokenExchangeUrl.
|
|
482
|
+
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
483
|
+
"https://oauth.example.com/token",
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("falls back to provider.tokenExchangeUrl when refreshUrl is empty string", async () => {
|
|
488
|
+
// Platform's Python `oauth_app.refresh_url or oauth_app.token_exchange_url`
|
|
489
|
+
// treats an empty string as unset. We use `||` (not `??`) so empty
|
|
490
|
+
// strings follow the same fallback path and never resolve to an empty
|
|
491
|
+
// endpoint.
|
|
492
|
+
await setupService("google");
|
|
493
|
+
mockProviders.get("google")!.refreshUrl = "";
|
|
494
|
+
|
|
495
|
+
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
496
|
+
Promise.resolve({
|
|
497
|
+
accessToken: "new-token-from-token-exchange-url",
|
|
498
|
+
expiresIn: 3600,
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
503
|
+
|
|
504
|
+
const callback = async (token: string) => {
|
|
505
|
+
if (token === "old-access-token") throw err401;
|
|
506
|
+
return `result-with-${token}`;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const result = await withValidToken("google", callback);
|
|
510
|
+
|
|
511
|
+
expect(result).toBe("result-with-new-token-from-token-exchange-url");
|
|
512
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
513
|
+
// Assert the refresh endpoint falls back to tokenExchangeUrl — NOT "".
|
|
514
|
+
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
515
|
+
"https://oauth.example.com/token",
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
});
|