@vellumai/assistant 0.4.49 → 0.4.51
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/docs/architecture/memory.md +180 -119
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +23 -18
- package/src/__tests__/credential-vault.test.ts +30 -18
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +166 -13
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +824 -31
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +363 -17
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +55 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +78 -18
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +85 -5
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +134 -3
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +44 -6
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +5 -4
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +11 -8
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +68 -10
- package/src/cli/commands/oauth/connections.ts +475 -105
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +18 -4
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +20 -22
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/loader.ts +6 -42
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +55 -2
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +39 -4
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -42
- package/src/daemon/server.ts +6 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-slash.ts +3 -5
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/email/providers/index.ts +2 -2
- package/src/instrument.ts +61 -1
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +25 -83
- package/src/memory/db-init.ts +32 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +40 -25
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +183 -31
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +214 -8
- package/src/oauth/token-persistence.ts +31 -16
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +72 -12
- package/src/runtime/invite-service.ts +43 -0
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +56 -11
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +9 -4
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +44 -19
- package/src/security/token-manager.ts +46 -39
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +22 -7
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +10 -3
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -10,7 +10,7 @@ describe("oauth provider behaviors", () => {
|
|
|
10
10
|
const service = resolveService("gmail");
|
|
11
11
|
const behavior = getProviderBehavior(service);
|
|
12
12
|
|
|
13
|
-
expect(service).toBe("integration:
|
|
13
|
+
expect(service).toBe("integration:google");
|
|
14
14
|
expect(behavior).toBeDefined();
|
|
15
15
|
expect(behavior?.injectionTemplates).toBeDefined();
|
|
16
16
|
expect(behavior?.injectionTemplates).toHaveLength(3);
|
|
@@ -34,9 +34,14 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
34
34
|
deleteSecureKeyAsync: mockDeleteSecureKeyAsync,
|
|
35
35
|
setSecureKeyAsync: mockSetSecureKeyAsync,
|
|
36
36
|
getSecureKey: (account: string) => secureKeyValues.get(account),
|
|
37
|
+
getSecureKeyAsync: (account: string) =>
|
|
38
|
+
Promise.resolve(secureKeyValues.get(account)),
|
|
37
39
|
}));
|
|
38
40
|
|
|
39
|
-
import {
|
|
41
|
+
import { eq } from "drizzle-orm";
|
|
42
|
+
|
|
43
|
+
import { getDb, initializeDb, resetDb, resetTestTables } from "../memory/db.js";
|
|
44
|
+
import { oauthProviders } from "../memory/schema/oauth.js";
|
|
40
45
|
import {
|
|
41
46
|
createConnection,
|
|
42
47
|
deleteApp,
|
|
@@ -46,8 +51,10 @@ import {
|
|
|
46
51
|
getAppByProviderAndClientId,
|
|
47
52
|
getConnection,
|
|
48
53
|
getConnectionByProvider,
|
|
54
|
+
getConnectionByProviderAndAccount,
|
|
49
55
|
getProvider,
|
|
50
56
|
isProviderConnected,
|
|
57
|
+
listActiveConnectionsByProvider,
|
|
51
58
|
listConnections,
|
|
52
59
|
registerProvider,
|
|
53
60
|
seedProviders,
|
|
@@ -137,7 +144,7 @@ describe("provider operations", () => {
|
|
|
137
144
|
});
|
|
138
145
|
});
|
|
139
146
|
|
|
140
|
-
test("updates
|
|
147
|
+
test("updates implementation fields while preserving user-customizable fields on re-seed", () => {
|
|
141
148
|
seedProviders([
|
|
142
149
|
{
|
|
143
150
|
providerKey: "github",
|
|
@@ -168,17 +175,134 @@ describe("provider operations", () => {
|
|
|
168
175
|
|
|
169
176
|
const row = getProvider("github");
|
|
170
177
|
expect(row).toBeDefined();
|
|
171
|
-
//
|
|
178
|
+
// Implementation fields should be overwritten by the re-seed
|
|
172
179
|
expect(row!.authUrl).toBe("https://github.com/login/oauth/authorize-v2");
|
|
173
180
|
expect(row!.tokenUrl).toBe(
|
|
174
181
|
"https://github.com/login/oauth/access_token-v2",
|
|
175
182
|
);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
expect(
|
|
183
|
+
// User-customizable fields (baseUrl, defaultScopes, scopePolicy) are
|
|
184
|
+
// preserved from the original insert — not overwritten on re-seed.
|
|
185
|
+
expect(row!.baseUrl).toBe("https://api.github.com");
|
|
186
|
+
expect(JSON.parse(row!.defaultScopes)).toEqual(["repo"]);
|
|
187
|
+
expect(JSON.parse(row!.scopePolicy)).toEqual({});
|
|
179
188
|
// createdAt should be preserved from the original insert
|
|
180
189
|
expect(row!.createdAt).toBe(originalCreatedAt);
|
|
181
190
|
});
|
|
191
|
+
|
|
192
|
+
test("persists pingUrl when provided", () => {
|
|
193
|
+
seedProviders([
|
|
194
|
+
{
|
|
195
|
+
providerKey: "github",
|
|
196
|
+
authUrl: "https://github.com/authorize",
|
|
197
|
+
tokenUrl: "https://github.com/token",
|
|
198
|
+
defaultScopes: ["repo"],
|
|
199
|
+
scopePolicy: {},
|
|
200
|
+
pingUrl: "https://api.github.com/user",
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
const row = getProvider("github");
|
|
204
|
+
expect(row!.pingUrl).toBe("https://api.github.com/user");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("pingUrl defaults to null when omitted", () => {
|
|
208
|
+
seedProviders([
|
|
209
|
+
{
|
|
210
|
+
providerKey: "github",
|
|
211
|
+
authUrl: "https://github.com/authorize",
|
|
212
|
+
tokenUrl: "https://github.com/token",
|
|
213
|
+
defaultScopes: ["repo"],
|
|
214
|
+
scopePolicy: {},
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
217
|
+
const row = getProvider("github");
|
|
218
|
+
expect(row!.pingUrl).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("preserves user-customizable fields while overwriting implementation fields on re-seed", () => {
|
|
222
|
+
// Initial seed with all fields
|
|
223
|
+
seedProviders([
|
|
224
|
+
{
|
|
225
|
+
providerKey: "github",
|
|
226
|
+
authUrl: "https://github.com/authorize",
|
|
227
|
+
tokenUrl: "https://github.com/token",
|
|
228
|
+
tokenEndpointAuthMethod: "client_secret_post",
|
|
229
|
+
defaultScopes: ["repo"],
|
|
230
|
+
scopePolicy: { required: ["repo"] },
|
|
231
|
+
userinfoUrl: "https://api.github.com/user",
|
|
232
|
+
baseUrl: "https://api.github.com",
|
|
233
|
+
extraParams: { prompt: "consent" },
|
|
234
|
+
callbackTransport: "loopback",
|
|
235
|
+
loopbackPort: 8765,
|
|
236
|
+
pingUrl: "https://api.github.com/user",
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
// Manually update user-customizable fields to simulate user edits
|
|
241
|
+
const db = getDb();
|
|
242
|
+
db.update(oauthProviders)
|
|
243
|
+
.set({
|
|
244
|
+
defaultScopes: JSON.stringify(["repo", "user", "gist"]),
|
|
245
|
+
scopePolicy: JSON.stringify({
|
|
246
|
+
required: ["repo"],
|
|
247
|
+
allowAdditionalScopes: true,
|
|
248
|
+
}),
|
|
249
|
+
userinfoUrl: "https://api.github.com/user/custom",
|
|
250
|
+
baseUrl: "https://custom.github.com/api",
|
|
251
|
+
})
|
|
252
|
+
.where(eq(oauthProviders.providerKey, "github"))
|
|
253
|
+
.run();
|
|
254
|
+
|
|
255
|
+
// Verify the manual updates took effect
|
|
256
|
+
const beforeReseed = getProvider("github");
|
|
257
|
+
expect(JSON.parse(beforeReseed!.defaultScopes)).toEqual([
|
|
258
|
+
"repo",
|
|
259
|
+
"user",
|
|
260
|
+
"gist",
|
|
261
|
+
]);
|
|
262
|
+
expect(beforeReseed!.userinfoUrl).toBe(
|
|
263
|
+
"https://api.github.com/user/custom",
|
|
264
|
+
);
|
|
265
|
+
expect(beforeReseed!.baseUrl).toBe("https://custom.github.com/api");
|
|
266
|
+
|
|
267
|
+
// Re-seed with updated implementation fields
|
|
268
|
+
seedProviders([
|
|
269
|
+
{
|
|
270
|
+
providerKey: "github",
|
|
271
|
+
authUrl: "https://github.com/authorize-v2",
|
|
272
|
+
tokenUrl: "https://github.com/token-v2",
|
|
273
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
274
|
+
defaultScopes: ["repo-only"],
|
|
275
|
+
scopePolicy: {},
|
|
276
|
+
userinfoUrl: "https://api.github.com/user-v2",
|
|
277
|
+
baseUrl: "https://api.github.com/v2",
|
|
278
|
+
extraParams: { prompt: "login" },
|
|
279
|
+
callbackTransport: "gateway",
|
|
280
|
+
loopbackPort: 9999,
|
|
281
|
+
pingUrl: "https://api.github.com/user-v2",
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
const row = getProvider("github");
|
|
286
|
+
expect(row).toBeDefined();
|
|
287
|
+
|
|
288
|
+
// User-customizable fields should retain their manual values
|
|
289
|
+
expect(JSON.parse(row!.defaultScopes)).toEqual(["repo", "user", "gist"]);
|
|
290
|
+
expect(JSON.parse(row!.scopePolicy)).toEqual({
|
|
291
|
+
required: ["repo"],
|
|
292
|
+
allowAdditionalScopes: true,
|
|
293
|
+
});
|
|
294
|
+
expect(row!.userinfoUrl).toBe("https://api.github.com/user/custom");
|
|
295
|
+
expect(row!.baseUrl).toBe("https://custom.github.com/api");
|
|
296
|
+
|
|
297
|
+
// Implementation fields should be overwritten from the seed data
|
|
298
|
+
expect(row!.authUrl).toBe("https://github.com/authorize-v2");
|
|
299
|
+
expect(row!.tokenUrl).toBe("https://github.com/token-v2");
|
|
300
|
+
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
301
|
+
expect(JSON.parse(row!.extraParams!)).toEqual({ prompt: "login" });
|
|
302
|
+
expect(row!.callbackTransport).toBe("gateway");
|
|
303
|
+
expect(row!.loopbackPort).toBe(9999);
|
|
304
|
+
expect(row!.pingUrl).toBe("https://api.github.com/user-v2");
|
|
305
|
+
});
|
|
182
306
|
});
|
|
183
307
|
|
|
184
308
|
describe("getProvider", () => {
|
|
@@ -279,13 +403,18 @@ describe("app operations", () => {
|
|
|
279
403
|
|
|
280
404
|
test("stores clientSecret in secure storage on new app creation", async () => {
|
|
281
405
|
seedTestProvider("github");
|
|
282
|
-
const app = await upsertApp("github", "client-abc",
|
|
406
|
+
const app = await upsertApp("github", "client-abc", {
|
|
407
|
+
clientSecretValue: "my-secret",
|
|
408
|
+
});
|
|
283
409
|
|
|
284
410
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
|
|
285
411
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
|
|
286
412
|
`oauth_app/${app.id}/client_secret`,
|
|
287
413
|
"my-secret",
|
|
288
414
|
);
|
|
415
|
+
expect(app.clientSecretCredentialPath).toBe(
|
|
416
|
+
`oauth_app/${app.id}/client_secret`,
|
|
417
|
+
);
|
|
289
418
|
});
|
|
290
419
|
|
|
291
420
|
test("stores clientSecret in secure storage when upserting an existing app", async () => {
|
|
@@ -293,11 +422,13 @@ describe("app operations", () => {
|
|
|
293
422
|
const first = await upsertApp("github", "client-abc");
|
|
294
423
|
mockSetSecureKeyAsync.mockClear();
|
|
295
424
|
|
|
296
|
-
await upsertApp("github", "client-abc",
|
|
425
|
+
await upsertApp("github", "client-abc", {
|
|
426
|
+
clientSecretValue: "updated-secret",
|
|
427
|
+
});
|
|
297
428
|
|
|
298
429
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
|
|
299
430
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
|
|
300
|
-
|
|
431
|
+
first.clientSecretCredentialPath,
|
|
301
432
|
"updated-secret",
|
|
302
433
|
);
|
|
303
434
|
});
|
|
@@ -307,9 +438,70 @@ describe("app operations", () => {
|
|
|
307
438
|
mockSetSecureKeyAsync.mockResolvedValueOnce(false);
|
|
308
439
|
|
|
309
440
|
await expect(
|
|
310
|
-
upsertApp("github", "client-abc", "bad-secret"),
|
|
441
|
+
upsertApp("github", "client-abc", { clientSecretValue: "bad-secret" }),
|
|
311
442
|
).rejects.toThrow("Failed to store client_secret in secure storage");
|
|
312
443
|
});
|
|
444
|
+
|
|
445
|
+
test("accepts clientSecretCredentialPath and verifies existence", async () => {
|
|
446
|
+
seedTestProvider("github");
|
|
447
|
+
secureKeyValues.set("custom/path", "stored-secret");
|
|
448
|
+
|
|
449
|
+
const app = await upsertApp("github", "client-abc", {
|
|
450
|
+
clientSecretCredentialPath: "custom/path",
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(app.clientSecretCredentialPath).toBe("custom/path");
|
|
454
|
+
// Should not have called setSecureKeyAsync since we only provided a path
|
|
455
|
+
expect(mockSetSecureKeyAsync).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("throws when clientSecretCredentialPath points to nonexistent secret", async () => {
|
|
459
|
+
seedTestProvider("github");
|
|
460
|
+
|
|
461
|
+
await expect(
|
|
462
|
+
upsertApp("github", "client-abc", {
|
|
463
|
+
clientSecretCredentialPath: "nonexistent/path",
|
|
464
|
+
}),
|
|
465
|
+
).rejects.toThrow("No secret found at credential path: nonexistent/path");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("throws when both clientSecretValue and clientSecretCredentialPath are provided", async () => {
|
|
469
|
+
seedTestProvider("github");
|
|
470
|
+
|
|
471
|
+
await expect(
|
|
472
|
+
upsertApp("github", "client-abc", {
|
|
473
|
+
clientSecretValue: "my-secret",
|
|
474
|
+
clientSecretCredentialPath: "custom/path",
|
|
475
|
+
}),
|
|
476
|
+
).rejects.toThrow(
|
|
477
|
+
"Cannot provide both clientSecretValue and clientSecretCredentialPath",
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("records default clientSecretCredentialPath when neither value nor path is provided", async () => {
|
|
482
|
+
seedTestProvider("github");
|
|
483
|
+
const app = await upsertApp("github", "client-abc");
|
|
484
|
+
|
|
485
|
+
expect(app.clientSecretCredentialPath).toBe(
|
|
486
|
+
`oauth_app/${app.id}/client_secret`,
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("updates clientSecretCredentialPath on existing row when path is provided", async () => {
|
|
491
|
+
seedTestProvider("github");
|
|
492
|
+
const first = await upsertApp("github", "client-abc");
|
|
493
|
+
expect(first.clientSecretCredentialPath).toBe(
|
|
494
|
+
`oauth_app/${first.id}/client_secret`,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
secureKeyValues.set("new/custom/path", "stored-secret");
|
|
498
|
+
const updated = await upsertApp("github", "client-abc", {
|
|
499
|
+
clientSecretCredentialPath: "new/custom/path",
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(updated.id).toBe(first.id);
|
|
503
|
+
expect(updated.clientSecretCredentialPath).toBe("new/custom/path");
|
|
504
|
+
});
|
|
313
505
|
});
|
|
314
506
|
|
|
315
507
|
describe("getApp", () => {
|
|
@@ -353,14 +545,29 @@ describe("app operations", () => {
|
|
|
353
545
|
expect(getApp(app.id)).toBeUndefined();
|
|
354
546
|
});
|
|
355
547
|
|
|
356
|
-
test("cleans up client_secret from secure storage", async () => {
|
|
548
|
+
test("cleans up client_secret from secure storage using stored path", async () => {
|
|
357
549
|
const app = await createTestApp("github", "client-1");
|
|
358
550
|
mockDeleteSecureKeyAsync.mockClear();
|
|
359
551
|
|
|
360
552
|
await deleteApp(app.id);
|
|
361
553
|
|
|
362
554
|
expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
|
|
363
|
-
|
|
555
|
+
app.clientSecretCredentialPath,
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("uses custom clientSecretCredentialPath when deleting", async () => {
|
|
560
|
+
seedTestProvider("github");
|
|
561
|
+
secureKeyValues.set("custom/secret/path", "the-secret");
|
|
562
|
+
const app = await upsertApp("github", "client-1", {
|
|
563
|
+
clientSecretCredentialPath: "custom/secret/path",
|
|
564
|
+
});
|
|
565
|
+
mockDeleteSecureKeyAsync.mockClear();
|
|
566
|
+
|
|
567
|
+
await deleteApp(app.id);
|
|
568
|
+
|
|
569
|
+
expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
|
|
570
|
+
"custom/secret/path",
|
|
364
571
|
);
|
|
365
572
|
});
|
|
366
573
|
|
|
@@ -506,6 +713,145 @@ describe("connection operations", () => {
|
|
|
506
713
|
});
|
|
507
714
|
});
|
|
508
715
|
|
|
716
|
+
describe("getConnectionByProviderAndAccount", () => {
|
|
717
|
+
test("returns the connection matching the given account", async () => {
|
|
718
|
+
const app = await createTestApp("github", "client-1");
|
|
719
|
+
|
|
720
|
+
const conn1 = createConnection({
|
|
721
|
+
oauthAppId: app.id,
|
|
722
|
+
providerKey: "github",
|
|
723
|
+
accountInfo: "user1@example.com",
|
|
724
|
+
grantedScopes: ["repo"],
|
|
725
|
+
hasRefreshToken: false,
|
|
726
|
+
createdAt: 1000,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
createConnection({
|
|
730
|
+
oauthAppId: app.id,
|
|
731
|
+
providerKey: "github",
|
|
732
|
+
accountInfo: "user2@example.com",
|
|
733
|
+
grantedScopes: ["repo"],
|
|
734
|
+
hasRefreshToken: false,
|
|
735
|
+
createdAt: 2000,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const result = getConnectionByProviderAndAccount(
|
|
739
|
+
"github",
|
|
740
|
+
"user1@example.com",
|
|
741
|
+
);
|
|
742
|
+
expect(result).toBeDefined();
|
|
743
|
+
expect(result!.id).toBe(conn1.id);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("falls back to getConnectionByProvider when accountInfo is undefined", async () => {
|
|
747
|
+
const app = await createTestApp("github", "client-1");
|
|
748
|
+
|
|
749
|
+
const conn = createConnection({
|
|
750
|
+
oauthAppId: app.id,
|
|
751
|
+
providerKey: "github",
|
|
752
|
+
accountInfo: "user@example.com",
|
|
753
|
+
grantedScopes: ["repo"],
|
|
754
|
+
hasRefreshToken: false,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const result = getConnectionByProviderAndAccount("github", undefined);
|
|
758
|
+
expect(result).toBeDefined();
|
|
759
|
+
expect(result!.id).toBe(conn.id);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("returns undefined when no connection matches the account", async () => {
|
|
763
|
+
const app = await createTestApp("github", "client-1");
|
|
764
|
+
|
|
765
|
+
createConnection({
|
|
766
|
+
oauthAppId: app.id,
|
|
767
|
+
providerKey: "github",
|
|
768
|
+
accountInfo: "user@example.com",
|
|
769
|
+
grantedScopes: ["repo"],
|
|
770
|
+
hasRefreshToken: false,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const result = getConnectionByProviderAndAccount(
|
|
774
|
+
"github",
|
|
775
|
+
"other@example.com",
|
|
776
|
+
);
|
|
777
|
+
expect(result).toBeUndefined();
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("skips revoked connections", async () => {
|
|
781
|
+
const app = await createTestApp("github", "client-1");
|
|
782
|
+
|
|
783
|
+
const conn = createConnection({
|
|
784
|
+
oauthAppId: app.id,
|
|
785
|
+
providerKey: "github",
|
|
786
|
+
accountInfo: "user@example.com",
|
|
787
|
+
grantedScopes: ["repo"],
|
|
788
|
+
hasRefreshToken: false,
|
|
789
|
+
});
|
|
790
|
+
updateConnection(conn.id, { status: "revoked" });
|
|
791
|
+
|
|
792
|
+
const result = getConnectionByProviderAndAccount(
|
|
793
|
+
"github",
|
|
794
|
+
"user@example.com",
|
|
795
|
+
);
|
|
796
|
+
expect(result).toBeUndefined();
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
describe("listActiveConnectionsByProvider", () => {
|
|
801
|
+
test("returns all active connections for a provider", async () => {
|
|
802
|
+
const app = await createTestApp("github", "client-1");
|
|
803
|
+
|
|
804
|
+
createConnection({
|
|
805
|
+
oauthAppId: app.id,
|
|
806
|
+
providerKey: "github",
|
|
807
|
+
accountInfo: "user1@example.com",
|
|
808
|
+
grantedScopes: ["repo"],
|
|
809
|
+
hasRefreshToken: false,
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
createConnection({
|
|
813
|
+
oauthAppId: app.id,
|
|
814
|
+
providerKey: "github",
|
|
815
|
+
accountInfo: "user2@example.com",
|
|
816
|
+
grantedScopes: ["repo"],
|
|
817
|
+
hasRefreshToken: false,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const results = listActiveConnectionsByProvider("github");
|
|
821
|
+
expect(results).toHaveLength(2);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("excludes revoked connections", async () => {
|
|
825
|
+
const app = await createTestApp("github", "client-1");
|
|
826
|
+
|
|
827
|
+
createConnection({
|
|
828
|
+
oauthAppId: app.id,
|
|
829
|
+
providerKey: "github",
|
|
830
|
+
accountInfo: "user1@example.com",
|
|
831
|
+
grantedScopes: ["repo"],
|
|
832
|
+
hasRefreshToken: false,
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const conn2 = createConnection({
|
|
836
|
+
oauthAppId: app.id,
|
|
837
|
+
providerKey: "github",
|
|
838
|
+
accountInfo: "user2@example.com",
|
|
839
|
+
grantedScopes: ["repo"],
|
|
840
|
+
hasRefreshToken: false,
|
|
841
|
+
});
|
|
842
|
+
updateConnection(conn2.id, { status: "revoked" });
|
|
843
|
+
|
|
844
|
+
const results = listActiveConnectionsByProvider("github");
|
|
845
|
+
expect(results).toHaveLength(1);
|
|
846
|
+
expect(results[0]!.accountInfo).toBe("user1@example.com");
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test("returns empty array when no active connections exist", () => {
|
|
850
|
+
const results = listActiveConnectionsByProvider("github");
|
|
851
|
+
expect(results).toHaveLength(0);
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
509
855
|
describe("isProviderConnected", () => {
|
|
510
856
|
test("returns true when active connection has an access token in secure storage", async () => {
|
|
511
857
|
const app = await createTestApp("github", "client-1");
|
|
@@ -518,7 +864,7 @@ describe("connection operations", () => {
|
|
|
518
864
|
|
|
519
865
|
secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok");
|
|
520
866
|
|
|
521
|
-
expect(isProviderConnected("github")).toBe(true);
|
|
867
|
+
expect(await isProviderConnected("github")).toBe(true);
|
|
522
868
|
});
|
|
523
869
|
|
|
524
870
|
test("returns false when active connection exists but access token is missing", async () => {
|
|
@@ -531,11 +877,11 @@ describe("connection operations", () => {
|
|
|
531
877
|
});
|
|
532
878
|
|
|
533
879
|
// No secure key set — simulates failed token write
|
|
534
|
-
expect(isProviderConnected("github")).toBe(false);
|
|
880
|
+
expect(await isProviderConnected("github")).toBe(false);
|
|
535
881
|
});
|
|
536
882
|
|
|
537
|
-
test("returns false when no connection exists", () => {
|
|
538
|
-
expect(isProviderConnected("github")).toBe(false);
|
|
883
|
+
test("returns false when no connection exists", async () => {
|
|
884
|
+
expect(await isProviderConnected("github")).toBe(false);
|
|
539
885
|
});
|
|
540
886
|
|
|
541
887
|
test("returns false when connection is revoked even with token in store", async () => {
|
|
@@ -550,7 +896,7 @@ describe("connection operations", () => {
|
|
|
550
896
|
updateConnection(conn.id, { status: "revoked" });
|
|
551
897
|
secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok");
|
|
552
898
|
|
|
553
|
-
expect(isProviderConnected("github")).toBe(false);
|
|
899
|
+
expect(await isProviderConnected("github")).toBe(false);
|
|
554
900
|
});
|
|
555
901
|
});
|
|
556
902
|
|
|
@@ -21,12 +21,14 @@ interface MockCallLog {
|
|
|
21
21
|
|
|
22
22
|
let mockCollectionExists: boolean;
|
|
23
23
|
let mockCollectionSize: number;
|
|
24
|
+
let mockUseNamedVectors: boolean;
|
|
24
25
|
let mockSentinelPayload: Record<string, unknown> | null;
|
|
25
26
|
let callLog: MockCallLog;
|
|
26
27
|
|
|
27
28
|
function resetMockState() {
|
|
28
29
|
mockCollectionExists = false;
|
|
29
30
|
mockCollectionSize = 384;
|
|
31
|
+
mockUseNamedVectors = false;
|
|
30
32
|
mockSentinelPayload = null;
|
|
31
33
|
callLog = {
|
|
32
34
|
collectionExists: 0,
|
|
@@ -51,7 +53,9 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
51
53
|
return {
|
|
52
54
|
config: {
|
|
53
55
|
params: {
|
|
54
|
-
vectors:
|
|
56
|
+
vectors: mockUseNamedVectors
|
|
57
|
+
? { dense: { size: mockCollectionSize } }
|
|
58
|
+
: { size: mockCollectionSize },
|
|
55
59
|
},
|
|
56
60
|
},
|
|
57
61
|
};
|
|
@@ -77,7 +81,12 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
77
81
|
mockSentinelPayload &&
|
|
78
82
|
opts.ids.includes("00000000-0000-0000-0000-000000000000")
|
|
79
83
|
) {
|
|
80
|
-
return [
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
id: "00000000-0000-0000-0000-000000000000",
|
|
87
|
+
payload: mockSentinelPayload,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
81
90
|
}
|
|
82
91
|
return [];
|
|
83
92
|
}
|
|
@@ -97,6 +106,7 @@ beforeEach(() => {
|
|
|
97
106
|
describe("Qdrant collection migration", () => {
|
|
98
107
|
test("deletes and recreates collection on dimension mismatch", async () => {
|
|
99
108
|
mockCollectionExists = true;
|
|
109
|
+
mockUseNamedVectors = true;
|
|
100
110
|
mockCollectionSize = 384; // Current collection has 384-dim vectors
|
|
101
111
|
|
|
102
112
|
const client = new VellumQdrantClient({
|
|
@@ -108,14 +118,16 @@ describe("Qdrant collection migration", () => {
|
|
|
108
118
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
109
119
|
});
|
|
110
120
|
|
|
111
|
-
await client.ensureCollection();
|
|
121
|
+
const result = await client.ensureCollection();
|
|
112
122
|
|
|
113
123
|
expect(callLog.deleteCollection).toBe(1);
|
|
114
124
|
expect(callLog.createCollection).toBe(1);
|
|
125
|
+
expect(result.migrated).toBe(true);
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
test("deletes and recreates collection on model-only mismatch", async () => {
|
|
118
129
|
mockCollectionExists = true;
|
|
130
|
+
mockUseNamedVectors = true;
|
|
119
131
|
mockCollectionSize = 768; // Same dimension
|
|
120
132
|
mockSentinelPayload = {
|
|
121
133
|
_meta: true,
|
|
@@ -131,16 +143,18 @@ describe("Qdrant collection migration", () => {
|
|
|
131
143
|
embeddingModel: "gemini:gemini-embedding-2-preview", // New model
|
|
132
144
|
});
|
|
133
145
|
|
|
134
|
-
await client.ensureCollection();
|
|
146
|
+
const result = await client.ensureCollection();
|
|
135
147
|
|
|
136
148
|
expect(callLog.deleteCollection).toBe(1);
|
|
137
149
|
expect(callLog.createCollection).toBe(1);
|
|
138
150
|
// Sentinel should be written for the new model
|
|
139
151
|
expect(callLog.upsert).toBe(1);
|
|
152
|
+
expect(result.migrated).toBe(true);
|
|
140
153
|
});
|
|
141
154
|
|
|
142
155
|
test("leaves collection untouched when dimensions and model match", async () => {
|
|
143
156
|
mockCollectionExists = true;
|
|
157
|
+
mockUseNamedVectors = true;
|
|
144
158
|
mockCollectionSize = 768;
|
|
145
159
|
mockSentinelPayload = {
|
|
146
160
|
_meta: true,
|
|
@@ -156,14 +170,16 @@ describe("Qdrant collection migration", () => {
|
|
|
156
170
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
157
171
|
});
|
|
158
172
|
|
|
159
|
-
await client.ensureCollection();
|
|
173
|
+
const result = await client.ensureCollection();
|
|
160
174
|
|
|
161
175
|
expect(callLog.deleteCollection).toBe(0);
|
|
162
176
|
expect(callLog.createCollection).toBe(0);
|
|
177
|
+
expect(result.migrated).toBe(false);
|
|
163
178
|
});
|
|
164
179
|
|
|
165
180
|
test("does not rebuild pre-existing collection without sentinel (graceful upgrade)", async () => {
|
|
166
181
|
mockCollectionExists = true;
|
|
182
|
+
mockUseNamedVectors = true;
|
|
167
183
|
mockCollectionSize = 768;
|
|
168
184
|
mockSentinelPayload = null; // No sentinel — pre-existing collection
|
|
169
185
|
|
|
@@ -176,11 +192,12 @@ describe("Qdrant collection migration", () => {
|
|
|
176
192
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
177
193
|
});
|
|
178
194
|
|
|
179
|
-
await client.ensureCollection();
|
|
195
|
+
const result = await client.ensureCollection();
|
|
180
196
|
|
|
181
197
|
// No sentinel found → no model mismatch → collection kept
|
|
182
198
|
expect(callLog.deleteCollection).toBe(0);
|
|
183
199
|
expect(callLog.createCollection).toBe(0);
|
|
200
|
+
expect(result.migrated).toBe(false);
|
|
184
201
|
});
|
|
185
202
|
|
|
186
203
|
test("writes sentinel point when creating a new collection", async () => {
|
|
@@ -195,11 +212,37 @@ describe("Qdrant collection migration", () => {
|
|
|
195
212
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
196
213
|
});
|
|
197
214
|
|
|
198
|
-
await client.ensureCollection();
|
|
215
|
+
const result = await client.ensureCollection();
|
|
199
216
|
|
|
200
217
|
expect(callLog.createCollection).toBe(1);
|
|
201
218
|
// Sentinel upsert should be called
|
|
202
219
|
expect(callLog.upsert).toBe(1);
|
|
220
|
+
// Fresh collection, not a migration
|
|
221
|
+
expect(result.migrated).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("deletes and recreates collection when migrating from unnamed to named vectors", async () => {
|
|
225
|
+
mockCollectionExists = true;
|
|
226
|
+
mockUseNamedVectors = false; // Legacy unnamed vectors
|
|
227
|
+
mockCollectionSize = 768;
|
|
228
|
+
|
|
229
|
+
const client = new VellumQdrantClient({
|
|
230
|
+
url: "http://localhost:6333",
|
|
231
|
+
collection: "memory",
|
|
232
|
+
vectorSize: 768, // Same dimension
|
|
233
|
+
onDisk: false,
|
|
234
|
+
quantization: "none",
|
|
235
|
+
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = await client.ensureCollection();
|
|
239
|
+
|
|
240
|
+
// Unnamed vectors should trigger delete + recreate with named vectors
|
|
241
|
+
expect(callLog.deleteCollection).toBe(1);
|
|
242
|
+
expect(callLog.createCollection).toBe(1);
|
|
243
|
+
// Sentinel should be written for the new collection
|
|
244
|
+
expect(callLog.upsert).toBe(1);
|
|
245
|
+
expect(result.migrated).toBe(true);
|
|
203
246
|
});
|
|
204
247
|
|
|
205
248
|
test("does not write sentinel when embeddingModel is not provided", async () => {
|
|
@@ -214,10 +257,12 @@ describe("Qdrant collection migration", () => {
|
|
|
214
257
|
// No embeddingModel
|
|
215
258
|
});
|
|
216
259
|
|
|
217
|
-
await client.ensureCollection();
|
|
260
|
+
const result = await client.ensureCollection();
|
|
218
261
|
|
|
219
262
|
expect(callLog.createCollection).toBe(1);
|
|
220
263
|
// No sentinel should be written
|
|
221
264
|
expect(callLog.upsert).toBe(0);
|
|
265
|
+
// Fresh collection, not a migration
|
|
266
|
+
expect(result.migrated).toBe(false);
|
|
222
267
|
});
|
|
223
268
|
});
|