@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
|
@@ -23,7 +23,11 @@ mock.module("../util/logger.js", () => ({
|
|
|
23
23
|
}),
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
findContactChannel,
|
|
28
|
+
getContact,
|
|
29
|
+
upsertContact,
|
|
30
|
+
} from "../contacts/contact-store.js";
|
|
27
31
|
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
28
32
|
import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
|
|
29
33
|
import {
|
|
@@ -54,12 +58,19 @@ function resetTables() {
|
|
|
54
58
|
getSqlite().run("DELETE FROM contacts");
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
/** Create a throwaway contact and return its ID, for use as the invite's contactId. */
|
|
62
|
+
function createTargetContact(displayName = "Target Contact"): string {
|
|
63
|
+
return upsertContact({ displayName, role: "contact" }).id;
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
describe("invite-redemption-service", () => {
|
|
58
67
|
beforeEach(resetTables);
|
|
59
68
|
|
|
60
69
|
test("redeems a valid invite and returns typed outcome", () => {
|
|
70
|
+
const targetContactId = createTargetContact();
|
|
61
71
|
const { rawToken, invite } = createInvite({
|
|
62
72
|
sourceChannel: "telegram",
|
|
73
|
+
contactId: targetContactId,
|
|
63
74
|
maxUses: 1,
|
|
64
75
|
});
|
|
65
76
|
|
|
@@ -79,8 +90,10 @@ describe("invite-redemption-service", () => {
|
|
|
79
90
|
});
|
|
80
91
|
|
|
81
92
|
test("marks channel as verified via invite on redemption", () => {
|
|
93
|
+
const targetContactId = createTargetContact();
|
|
82
94
|
const { rawToken } = createInvite({
|
|
83
95
|
sourceChannel: "telegram",
|
|
96
|
+
contactId: targetContactId,
|
|
84
97
|
maxUses: 1,
|
|
85
98
|
});
|
|
86
99
|
|
|
@@ -104,9 +117,11 @@ describe("invite-redemption-service", () => {
|
|
|
104
117
|
});
|
|
105
118
|
|
|
106
119
|
test("marks channel as verified via invite on 6-digit code redemption", () => {
|
|
120
|
+
const targetContactId = createTargetContact();
|
|
107
121
|
const inviteCode = "123456";
|
|
108
122
|
createInvite({
|
|
109
123
|
sourceChannel: "telegram",
|
|
124
|
+
contactId: targetContactId,
|
|
110
125
|
maxUses: 1,
|
|
111
126
|
inviteCodeHash: hashVoiceCode(inviteCode),
|
|
112
127
|
});
|
|
@@ -141,9 +156,11 @@ describe("invite-redemption-service", () => {
|
|
|
141
156
|
});
|
|
142
157
|
|
|
143
158
|
test("returns expired for an expired invite", () => {
|
|
159
|
+
const targetContactId = createTargetContact();
|
|
144
160
|
// Create an invite that expired 1 ms ago
|
|
145
161
|
const { rawToken } = createInvite({
|
|
146
162
|
sourceChannel: "telegram",
|
|
163
|
+
contactId: targetContactId,
|
|
147
164
|
maxUses: 1,
|
|
148
165
|
expiresInMs: -1,
|
|
149
166
|
});
|
|
@@ -158,8 +175,10 @@ describe("invite-redemption-service", () => {
|
|
|
158
175
|
});
|
|
159
176
|
|
|
160
177
|
test("returns revoked for a revoked invite", () => {
|
|
178
|
+
const targetContactId = createTargetContact();
|
|
161
179
|
const { rawToken, invite } = createInvite({
|
|
162
180
|
sourceChannel: "telegram",
|
|
181
|
+
contactId: targetContactId,
|
|
163
182
|
maxUses: 1,
|
|
164
183
|
});
|
|
165
184
|
revokeStoreFn(invite.id);
|
|
@@ -174,8 +193,10 @@ describe("invite-redemption-service", () => {
|
|
|
174
193
|
});
|
|
175
194
|
|
|
176
195
|
test("returns max_uses_reached when invite is fully consumed", () => {
|
|
196
|
+
const targetContactId = createTargetContact();
|
|
177
197
|
const { rawToken } = createInvite({
|
|
178
198
|
sourceChannel: "telegram",
|
|
199
|
+
contactId: targetContactId,
|
|
179
200
|
maxUses: 1,
|
|
180
201
|
});
|
|
181
202
|
|
|
@@ -198,8 +219,10 @@ describe("invite-redemption-service", () => {
|
|
|
198
219
|
});
|
|
199
220
|
|
|
200
221
|
test("returns channel_mismatch when redeeming on wrong channel", () => {
|
|
222
|
+
const targetContactId = createTargetContact();
|
|
201
223
|
const { rawToken } = createInvite({
|
|
202
224
|
sourceChannel: "telegram",
|
|
225
|
+
contactId: targetContactId,
|
|
203
226
|
maxUses: 1,
|
|
204
227
|
});
|
|
205
228
|
|
|
@@ -213,8 +236,10 @@ describe("invite-redemption-service", () => {
|
|
|
213
236
|
});
|
|
214
237
|
|
|
215
238
|
test("returns missing_identity when no externalUserId or externalChatId", () => {
|
|
239
|
+
const targetContactId = createTargetContact();
|
|
216
240
|
const { rawToken } = createInvite({
|
|
217
241
|
sourceChannel: "telegram",
|
|
242
|
+
contactId: targetContactId,
|
|
218
243
|
maxUses: 1,
|
|
219
244
|
});
|
|
220
245
|
|
|
@@ -227,16 +252,18 @@ describe("invite-redemption-service", () => {
|
|
|
227
252
|
});
|
|
228
253
|
|
|
229
254
|
test("returns already_member when user is already an active member", () => {
|
|
230
|
-
|
|
255
|
+
// Pre-create an active member and find their contact
|
|
256
|
+
const member = upsertContactChannel({
|
|
231
257
|
sourceChannel: "telegram",
|
|
232
|
-
|
|
258
|
+
externalUserId: "existing-user",
|
|
259
|
+
status: "active",
|
|
233
260
|
});
|
|
234
261
|
|
|
235
|
-
//
|
|
236
|
-
|
|
262
|
+
// Create an invite targeting the same contact that owns the channel
|
|
263
|
+
const { rawToken } = createInvite({
|
|
237
264
|
sourceChannel: "telegram",
|
|
238
|
-
|
|
239
|
-
|
|
265
|
+
contactId: member!.contact.id,
|
|
266
|
+
maxUses: 5,
|
|
240
267
|
});
|
|
241
268
|
|
|
242
269
|
const outcome = redeemInvite({
|
|
@@ -257,30 +284,145 @@ describe("invite-redemption-service", () => {
|
|
|
257
284
|
});
|
|
258
285
|
|
|
259
286
|
test("returns invalid_token for a blocked member to avoid leaking membership status", () => {
|
|
287
|
+
// Pre-create a blocked member and find their contact
|
|
288
|
+
const member = upsertContactChannel({
|
|
289
|
+
sourceChannel: "telegram",
|
|
290
|
+
externalUserId: "blocked-user",
|
|
291
|
+
status: "blocked",
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Create an invite targeting the same contact that owns the channel
|
|
260
295
|
const { rawToken } = createInvite({
|
|
261
296
|
sourceChannel: "telegram",
|
|
297
|
+
contactId: member!.contact.id,
|
|
262
298
|
maxUses: 5,
|
|
263
299
|
});
|
|
264
300
|
|
|
265
|
-
|
|
266
|
-
|
|
301
|
+
const outcome = redeemInvite({
|
|
302
|
+
rawToken,
|
|
267
303
|
sourceChannel: "telegram",
|
|
268
304
|
externalUserId: "blocked-user",
|
|
269
|
-
status: "blocked",
|
|
270
305
|
});
|
|
271
306
|
|
|
307
|
+
expect(outcome).toEqual({ ok: false, reason: "invalid_token" });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("binds redeemer to the invite's target contact, not the guardian", () => {
|
|
311
|
+
// Pre-create a guardian contact with a revoked telegram channel
|
|
312
|
+
const guardianContact = upsertContact({
|
|
313
|
+
displayName: "Guardian",
|
|
314
|
+
role: "guardian",
|
|
315
|
+
channels: [
|
|
316
|
+
{
|
|
317
|
+
type: "telegram",
|
|
318
|
+
address: "guardian-tg-id",
|
|
319
|
+
externalUserId: "guardian-tg-id",
|
|
320
|
+
status: "revoked",
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Create a separate target contact "Mom"
|
|
326
|
+
const momContact = upsertContact({
|
|
327
|
+
displayName: "Mom",
|
|
328
|
+
role: "contact",
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Create an invite targeting Mom's contact
|
|
332
|
+
const { rawToken } = createInvite({
|
|
333
|
+
sourceChannel: "telegram",
|
|
334
|
+
contactId: momContact.id,
|
|
335
|
+
maxUses: 5,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Redeem using the guardian's Telegram identity
|
|
272
339
|
const outcome = redeemInvite({
|
|
273
340
|
rawToken,
|
|
274
341
|
sourceChannel: "telegram",
|
|
275
|
-
externalUserId: "
|
|
342
|
+
externalUserId: "guardian-tg-id",
|
|
276
343
|
});
|
|
277
344
|
|
|
278
|
-
|
|
345
|
+
// Should succeed — redeemer's channel is bound to Mom
|
|
346
|
+
expect(outcome.ok).toBe(true);
|
|
347
|
+
expect((outcome as { type: string }).type).toBe("redeemed");
|
|
348
|
+
|
|
349
|
+
// Verify the redeemer's Telegram ID is now bound to Mom's contact
|
|
350
|
+
const result = findContactChannel({
|
|
351
|
+
channelType: "telegram",
|
|
352
|
+
externalUserId: "guardian-tg-id",
|
|
353
|
+
});
|
|
354
|
+
expect(result).not.toBeNull();
|
|
355
|
+
expect(result!.contact.id).toBe(momContact.id);
|
|
356
|
+
expect(result!.channel.status).toBe("active");
|
|
357
|
+
|
|
358
|
+
// Verify the original guardian contact was NOT modified
|
|
359
|
+
const guardian = getContact(guardianContact.id);
|
|
360
|
+
expect(guardian).not.toBeNull();
|
|
361
|
+
expect(guardian!.role).toBe("guardian");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("binds redeemer to the invite's target contact via 6-digit code, not the guardian", () => {
|
|
365
|
+
// Pre-create a guardian contact with a revoked telegram channel
|
|
366
|
+
const guardianContact = upsertContact({
|
|
367
|
+
displayName: "Guardian",
|
|
368
|
+
role: "guardian",
|
|
369
|
+
channels: [
|
|
370
|
+
{
|
|
371
|
+
type: "telegram",
|
|
372
|
+
address: "guardian-code-id",
|
|
373
|
+
externalUserId: "guardian-code-id",
|
|
374
|
+
status: "revoked",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Create a separate target contact "Mom"
|
|
380
|
+
const momContact = upsertContact({
|
|
381
|
+
displayName: "Mom",
|
|
382
|
+
role: "contact",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Create an invite targeting Mom's contact with a 6-digit code
|
|
386
|
+
const code = "123456";
|
|
387
|
+
const inviteCodeHash = hashVoiceCode(code);
|
|
388
|
+
createInvite({
|
|
389
|
+
sourceChannel: "telegram",
|
|
390
|
+
contactId: momContact.id,
|
|
391
|
+
maxUses: 5,
|
|
392
|
+
inviteCodeHash,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Redeem using the guardian's Telegram identity
|
|
396
|
+
const outcome = redeemInviteByCode({
|
|
397
|
+
code,
|
|
398
|
+
sourceChannel: "telegram",
|
|
399
|
+
externalUserId: "guardian-code-id",
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Should succeed — redeemer's channel is bound to Mom
|
|
403
|
+
expect(outcome.ok).toBe(true);
|
|
404
|
+
expect((outcome as { type: string }).type).toBe("redeemed");
|
|
405
|
+
|
|
406
|
+
// Verify the redeemer's Telegram ID is now bound to Mom's contact
|
|
407
|
+
const result = findContactChannel({
|
|
408
|
+
channelType: "telegram",
|
|
409
|
+
externalUserId: "guardian-code-id",
|
|
410
|
+
});
|
|
411
|
+
expect(result).not.toBeNull();
|
|
412
|
+
expect(result!.contact.id).toBe(momContact.id);
|
|
413
|
+
expect(result!.channel.status).toBe("active");
|
|
414
|
+
|
|
415
|
+
// Verify the original guardian contact was NOT modified
|
|
416
|
+
const guardian = getContact(guardianContact.id);
|
|
417
|
+
expect(guardian).not.toBeNull();
|
|
418
|
+
expect(guardian!.role).toBe("guardian");
|
|
279
419
|
});
|
|
280
420
|
|
|
281
421
|
test("does not return already_member for a revoked member", () => {
|
|
422
|
+
const targetContactId = createTargetContact();
|
|
282
423
|
const { rawToken } = createInvite({
|
|
283
424
|
sourceChannel: "telegram",
|
|
425
|
+
contactId: targetContactId,
|
|
284
426
|
maxUses: 5,
|
|
285
427
|
});
|
|
286
428
|
|
|
@@ -306,8 +448,10 @@ describe("invite-redemption-service", () => {
|
|
|
306
448
|
});
|
|
307
449
|
|
|
308
450
|
test("raw token is not present in the outcome object", () => {
|
|
451
|
+
const targetContactId = createTargetContact();
|
|
309
452
|
const { rawToken } = createInvite({
|
|
310
453
|
sourceChannel: "telegram",
|
|
454
|
+
contactId: targetContactId,
|
|
311
455
|
maxUses: 1,
|
|
312
456
|
});
|
|
313
457
|
|
|
@@ -323,7 +467,12 @@ describe("invite-redemption-service", () => {
|
|
|
323
467
|
});
|
|
324
468
|
|
|
325
469
|
test("channel enforcement blocks cross-channel redemption (voice invite via slack)", () => {
|
|
326
|
-
const
|
|
470
|
+
const targetContactId = createTargetContact();
|
|
471
|
+
const { rawToken } = createInvite({
|
|
472
|
+
sourceChannel: "phone",
|
|
473
|
+
contactId: targetContactId,
|
|
474
|
+
maxUses: 1,
|
|
475
|
+
});
|
|
327
476
|
|
|
328
477
|
const outcome = redeemInvite({
|
|
329
478
|
rawToken,
|
|
@@ -353,9 +502,11 @@ describe("invite-redemption-service", () => {
|
|
|
353
502
|
});
|
|
354
503
|
|
|
355
504
|
test("returns expired for an active member with an expired invite token", () => {
|
|
505
|
+
const targetContactId = createTargetContact();
|
|
356
506
|
// Create an expired invite
|
|
357
507
|
const { rawToken } = createInvite({
|
|
358
508
|
sourceChannel: "telegram",
|
|
509
|
+
contactId: targetContactId,
|
|
359
510
|
maxUses: 5,
|
|
360
511
|
expiresInMs: -1,
|
|
361
512
|
});
|
|
@@ -378,9 +529,11 @@ describe("invite-redemption-service", () => {
|
|
|
378
529
|
});
|
|
379
530
|
|
|
380
531
|
test("returns channel_mismatch for an active member with a valid token for a different channel", () => {
|
|
532
|
+
const targetContactId = createTargetContact();
|
|
381
533
|
// Create an invite for voice
|
|
382
534
|
const { rawToken } = createInvite({
|
|
383
535
|
sourceChannel: "phone",
|
|
536
|
+
contactId: targetContactId,
|
|
384
537
|
maxUses: 5,
|
|
385
538
|
});
|
|
386
539
|
|
|
@@ -41,16 +41,34 @@ mock.module("../telegram/bot-username.js", () => ({
|
|
|
41
41
|
getTelegramBotUsername: () => mockTelegramBotUsername,
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
|
+
// Mock startInviteCall from call-domain — test env lacks Twilio credentials.
|
|
45
|
+
let mockStartInviteCallResult:
|
|
46
|
+
| { ok: true; callSid: string }
|
|
47
|
+
| { ok: false; error: string; status?: number } = {
|
|
48
|
+
ok: true,
|
|
49
|
+
callSid: "CA_test_sid_123",
|
|
50
|
+
};
|
|
51
|
+
mock.module("../calls/call-domain.js", () => ({
|
|
52
|
+
startInviteCall: async () => mockStartInviteCallResult,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
import { upsertContact } from "../contacts/contact-store.js";
|
|
44
56
|
import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
|
|
45
57
|
import {
|
|
46
58
|
handleCreateInvite,
|
|
47
59
|
handleListInvites,
|
|
48
60
|
handleRedeemInvite,
|
|
49
61
|
handleRevokeInvite,
|
|
62
|
+
handleTriggerInviteCall,
|
|
50
63
|
} from "../runtime/routes/invite-routes.js";
|
|
51
64
|
|
|
52
65
|
initializeDb();
|
|
53
66
|
|
|
67
|
+
/** Create a throwaway contact and return its ID, for use as the invite's contactId. */
|
|
68
|
+
function createTargetContact(displayName = "Test Contact"): string {
|
|
69
|
+
return upsertContact({ displayName, role: "contact" }).id;
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
afterAll(() => {
|
|
55
73
|
resetDb();
|
|
56
74
|
try {
|
|
@@ -79,6 +97,7 @@ describe("ingress invite HTTP routes", () => {
|
|
|
79
97
|
headers: { "Content-Type": "application/json" },
|
|
80
98
|
body: JSON.stringify({
|
|
81
99
|
sourceChannel: "telegram",
|
|
100
|
+
contactId: createTargetContact(),
|
|
82
101
|
note: "Test invite",
|
|
83
102
|
maxUses: 5,
|
|
84
103
|
}),
|
|
@@ -108,6 +127,7 @@ describe("ingress invite HTTP routes", () => {
|
|
|
108
127
|
headers: { "Content-Type": "application/json" },
|
|
109
128
|
body: JSON.stringify({
|
|
110
129
|
sourceChannel: "telegram",
|
|
130
|
+
contactId: createTargetContact(),
|
|
111
131
|
note: "Share link test",
|
|
112
132
|
}),
|
|
113
133
|
});
|
|
@@ -151,14 +171,20 @@ describe("ingress invite HTTP routes", () => {
|
|
|
151
171
|
new Request("http://localhost/v1/contacts/invites", {
|
|
152
172
|
method: "POST",
|
|
153
173
|
headers: { "Content-Type": "application/json" },
|
|
154
|
-
body: JSON.stringify({
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
sourceChannel: "telegram",
|
|
176
|
+
contactId: createTargetContact(),
|
|
177
|
+
}),
|
|
155
178
|
}),
|
|
156
179
|
);
|
|
157
180
|
await handleCreateInvite(
|
|
158
181
|
new Request("http://localhost/v1/contacts/invites", {
|
|
159
182
|
method: "POST",
|
|
160
183
|
headers: { "Content-Type": "application/json" },
|
|
161
|
-
body: JSON.stringify({
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
sourceChannel: "telegram",
|
|
186
|
+
contactId: createTargetContact(),
|
|
187
|
+
}),
|
|
162
188
|
}),
|
|
163
189
|
);
|
|
164
190
|
|
|
@@ -177,7 +203,10 @@ describe("ingress invite HTTP routes", () => {
|
|
|
177
203
|
new Request("http://localhost/v1/contacts/invites", {
|
|
178
204
|
method: "POST",
|
|
179
205
|
headers: { "Content-Type": "application/json" },
|
|
180
|
-
body: JSON.stringify({
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
sourceChannel: "telegram",
|
|
208
|
+
contactId: createTargetContact(),
|
|
209
|
+
}),
|
|
181
210
|
}),
|
|
182
211
|
);
|
|
183
212
|
const created = (await createRes.json()) as { invite: { id: string } };
|
|
@@ -202,7 +231,11 @@ describe("ingress invite HTTP routes", () => {
|
|
|
202
231
|
new Request("http://localhost/v1/contacts/invites", {
|
|
203
232
|
method: "POST",
|
|
204
233
|
headers: { "Content-Type": "application/json" },
|
|
205
|
-
body: JSON.stringify({
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
sourceChannel: "telegram",
|
|
236
|
+
contactId: createTargetContact(),
|
|
237
|
+
maxUses: 1,
|
|
238
|
+
}),
|
|
206
239
|
}),
|
|
207
240
|
);
|
|
208
241
|
const created = (await createRes.json()) as { invite: { token: string } };
|
|
@@ -270,7 +303,10 @@ describe("ingress service shared logic", () => {
|
|
|
270
303
|
new Request("http://localhost/v1/contacts/invites", {
|
|
271
304
|
method: "POST",
|
|
272
305
|
headers: { "Content-Type": "application/json" },
|
|
273
|
-
body: JSON.stringify({
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
sourceChannel: "telegram",
|
|
308
|
+
contactId: createTargetContact(),
|
|
309
|
+
}),
|
|
274
310
|
}),
|
|
275
311
|
);
|
|
276
312
|
const created = (await createRes.json()) as {
|
|
@@ -300,6 +336,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
300
336
|
headers: { "Content-Type": "application/json" },
|
|
301
337
|
body: JSON.stringify({
|
|
302
338
|
sourceChannel: "phone",
|
|
339
|
+
contactId: createTargetContact(),
|
|
303
340
|
expectedExternalUserId: "+15551234567",
|
|
304
341
|
friendName: "Alice",
|
|
305
342
|
guardianName: "Bob",
|
|
@@ -336,6 +373,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
336
373
|
headers: { "Content-Type": "application/json" },
|
|
337
374
|
body: JSON.stringify({
|
|
338
375
|
sourceChannel: "phone",
|
|
376
|
+
contactId: createTargetContact(),
|
|
339
377
|
friendName: "Alice",
|
|
340
378
|
guardianName: "Bob",
|
|
341
379
|
}),
|
|
@@ -355,6 +393,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
355
393
|
headers: { "Content-Type": "application/json" },
|
|
356
394
|
body: JSON.stringify({
|
|
357
395
|
sourceChannel: "phone",
|
|
396
|
+
contactId: createTargetContact(),
|
|
358
397
|
expectedExternalUserId: "not-a-phone-number",
|
|
359
398
|
friendName: "Alice",
|
|
360
399
|
guardianName: "Bob",
|
|
@@ -375,6 +414,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
375
414
|
headers: { "Content-Type": "application/json" },
|
|
376
415
|
body: JSON.stringify({
|
|
377
416
|
sourceChannel: "phone",
|
|
417
|
+
contactId: createTargetContact(),
|
|
378
418
|
expectedExternalUserId: "+15551234567",
|
|
379
419
|
guardianName: "Bob",
|
|
380
420
|
}),
|
|
@@ -394,6 +434,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
394
434
|
headers: { "Content-Type": "application/json" },
|
|
395
435
|
body: JSON.stringify({
|
|
396
436
|
sourceChannel: "phone",
|
|
437
|
+
contactId: createTargetContact(),
|
|
397
438
|
expectedExternalUserId: "+15551234567",
|
|
398
439
|
friendName: "Alice",
|
|
399
440
|
}),
|
|
@@ -413,6 +454,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
413
454
|
headers: { "Content-Type": "application/json" },
|
|
414
455
|
body: JSON.stringify({
|
|
415
456
|
sourceChannel: "phone",
|
|
457
|
+
contactId: createTargetContact(),
|
|
416
458
|
expectedExternalUserId: "+15551234567",
|
|
417
459
|
friendName: "Alice",
|
|
418
460
|
guardianName: "Bob",
|
|
@@ -436,6 +478,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
436
478
|
headers: { "Content-Type": "application/json" },
|
|
437
479
|
body: JSON.stringify({
|
|
438
480
|
sourceChannel: "phone",
|
|
481
|
+
contactId: createTargetContact(),
|
|
439
482
|
expectedExternalUserId: "+15551234567",
|
|
440
483
|
friendName: "Alice",
|
|
441
484
|
guardianName: "Bob",
|
|
@@ -460,6 +503,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
460
503
|
headers: { "Content-Type": "application/json" },
|
|
461
504
|
body: JSON.stringify({
|
|
462
505
|
sourceChannel: "phone",
|
|
506
|
+
contactId: createTargetContact(),
|
|
463
507
|
expectedExternalUserId: "+15551234567",
|
|
464
508
|
friendName: "Alice",
|
|
465
509
|
guardianName: "Bob",
|
|
@@ -517,6 +561,7 @@ describe("voice invite HTTP routes", () => {
|
|
|
517
561
|
headers: { "Content-Type": "application/json" },
|
|
518
562
|
body: JSON.stringify({
|
|
519
563
|
sourceChannel: "phone",
|
|
564
|
+
contactId: createTargetContact(),
|
|
520
565
|
expectedExternalUserId: "+15551234567",
|
|
521
566
|
friendName: "Alice",
|
|
522
567
|
guardianName: "Bob",
|
|
@@ -540,4 +585,120 @@ describe("voice invite HTTP routes", () => {
|
|
|
540
585
|
expect(res.status).toBe(400);
|
|
541
586
|
expect(body.ok).toBe(false);
|
|
542
587
|
});
|
|
588
|
+
|
|
589
|
+
test("voice invite creation returns guardianInstruction with friend name", async () => {
|
|
590
|
+
const req = new Request("http://localhost/v1/contacts/invites", {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "Content-Type": "application/json" },
|
|
593
|
+
body: JSON.stringify({
|
|
594
|
+
sourceChannel: "phone",
|
|
595
|
+
contactId: createTargetContact(),
|
|
596
|
+
expectedExternalUserId: "+15551234567",
|
|
597
|
+
friendName: "Alice",
|
|
598
|
+
guardianName: "Bob",
|
|
599
|
+
}),
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const res = await handleCreateInvite(req);
|
|
603
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
604
|
+
|
|
605
|
+
expect(res.status).toBe(201);
|
|
606
|
+
expect(body.ok).toBe(true);
|
|
607
|
+
const invite = body.invite as Record<string, unknown>;
|
|
608
|
+
expect(invite.guardianInstruction).toBe(
|
|
609
|
+
"Alice will need this code when they answer. Share it with them first.",
|
|
610
|
+
);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
// Trigger invite call endpoint
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
describe("POST /v1/contacts/invites/:id/call", () => {
|
|
619
|
+
beforeEach(() => {
|
|
620
|
+
resetTables();
|
|
621
|
+
mockStartInviteCallResult = { ok: true, callSid: "CA_test_sid_123" };
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("triggers a call for an active phone invite", async () => {
|
|
625
|
+
const createRes = await handleCreateInvite(
|
|
626
|
+
new Request("http://localhost/v1/contacts/invites", {
|
|
627
|
+
method: "POST",
|
|
628
|
+
headers: { "Content-Type": "application/json" },
|
|
629
|
+
body: JSON.stringify({
|
|
630
|
+
sourceChannel: "phone",
|
|
631
|
+
contactId: createTargetContact(),
|
|
632
|
+
expectedExternalUserId: "+15551234567",
|
|
633
|
+
friendName: "Alice",
|
|
634
|
+
guardianName: "Bob",
|
|
635
|
+
}),
|
|
636
|
+
}),
|
|
637
|
+
);
|
|
638
|
+
const created = (await createRes.json()) as { invite: { id: string } };
|
|
639
|
+
|
|
640
|
+
const res = await handleTriggerInviteCall(created.invite.id);
|
|
641
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
642
|
+
|
|
643
|
+
expect(res.status).toBe(200);
|
|
644
|
+
expect(body.ok).toBe(true);
|
|
645
|
+
expect(body.callSid).toBe("CA_test_sid_123");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("returns 400 for non-existent invite", async () => {
|
|
649
|
+
const res = await handleTriggerInviteCall("nonexistent-id");
|
|
650
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
651
|
+
|
|
652
|
+
expect(res.status).toBe(400);
|
|
653
|
+
expect(body.ok).toBe(false);
|
|
654
|
+
expect(body.error).toBe("Invite not found");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("returns 400 for a revoked (non-active) invite", async () => {
|
|
658
|
+
const createRes = await handleCreateInvite(
|
|
659
|
+
new Request("http://localhost/v1/contacts/invites", {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: { "Content-Type": "application/json" },
|
|
662
|
+
body: JSON.stringify({
|
|
663
|
+
sourceChannel: "phone",
|
|
664
|
+
contactId: createTargetContact(),
|
|
665
|
+
expectedExternalUserId: "+15551234567",
|
|
666
|
+
friendName: "Alice",
|
|
667
|
+
guardianName: "Bob",
|
|
668
|
+
}),
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
const created = (await createRes.json()) as { invite: { id: string } };
|
|
672
|
+
|
|
673
|
+
// Revoke the invite
|
|
674
|
+
handleRevokeInvite(created.invite.id);
|
|
675
|
+
|
|
676
|
+
const res = await handleTriggerInviteCall(created.invite.id);
|
|
677
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
678
|
+
|
|
679
|
+
expect(res.status).toBe(400);
|
|
680
|
+
expect(body.ok).toBe(false);
|
|
681
|
+
expect(body.error).toBe("Invite is not active");
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("returns 400 for a non-phone invite", async () => {
|
|
685
|
+
const createRes = await handleCreateInvite(
|
|
686
|
+
new Request("http://localhost/v1/contacts/invites", {
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: { "Content-Type": "application/json" },
|
|
689
|
+
body: JSON.stringify({
|
|
690
|
+
sourceChannel: "telegram",
|
|
691
|
+
contactId: createTargetContact(),
|
|
692
|
+
}),
|
|
693
|
+
}),
|
|
694
|
+
);
|
|
695
|
+
const created = (await createRes.json()) as { invite: { id: string } };
|
|
696
|
+
|
|
697
|
+
const res = await handleTriggerInviteCall(created.invite.id);
|
|
698
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
699
|
+
|
|
700
|
+
expect(res.status).toBe(400);
|
|
701
|
+
expect(body.ok).toBe(false);
|
|
702
|
+
expect(body.error).toBe("Only phone invites support call triggering");
|
|
703
|
+
});
|
|
543
704
|
});
|
|
@@ -264,7 +264,7 @@ describe("keychain-broker-client", () => {
|
|
|
264
264
|
|
|
265
265
|
const client = createBrokerClient();
|
|
266
266
|
const result = await client.set("my-key", "new-value");
|
|
267
|
-
expect(result).
|
|
267
|
+
expect(result).toEqual({ status: "ok" });
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
test("del returns true on success", async () => {
|
|
@@ -434,11 +434,11 @@ describe("keychain-broker-client", () => {
|
|
|
434
434
|
expect(result).toBeNull();
|
|
435
435
|
});
|
|
436
436
|
|
|
437
|
-
test("set returns
|
|
437
|
+
test("set returns unreachable when socket file does not exist", async () => {
|
|
438
438
|
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
439
439
|
const client = createBrokerClient();
|
|
440
440
|
const result = await client.set("test-key", "value");
|
|
441
|
-
expect(result).
|
|
441
|
+
expect(result).toEqual({ status: "unreachable" });
|
|
442
442
|
});
|
|
443
443
|
|
|
444
444
|
test("del returns false when socket file does not exist", async () => {
|
|
@@ -470,7 +470,7 @@ describe("keychain-broker-client", () => {
|
|
|
470
470
|
}
|
|
471
471
|
const client = createBrokerClient();
|
|
472
472
|
expect(await client.get("key")).toBeNull();
|
|
473
|
-
expect(await client.set("key", "val")).
|
|
473
|
+
expect(await client.set("key", "val")).toEqual({ status: "unreachable" });
|
|
474
474
|
expect(await client.del("key")).toBe(false);
|
|
475
475
|
expect(await client.list()).toEqual([]);
|
|
476
476
|
expect(await client.ping()).toBeNull();
|