@vellumai/assistant 0.11.1 → 0.11.2-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
9
|
|
|
10
|
+
import type { PairingResult } from "../conversation-pairing.js";
|
|
10
11
|
import type { NotificationSignal } from "../signal.js";
|
|
11
12
|
import type {
|
|
12
13
|
ChannelAdapter,
|
|
14
|
+
ChannelDeliveryObserver,
|
|
13
15
|
ChannelDeliveryPayload,
|
|
14
16
|
ChannelDestination,
|
|
15
17
|
DeliveryResult,
|
|
@@ -36,14 +38,27 @@ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
|
|
|
36
38
|
getGuardianDelivery: async () => null,
|
|
37
39
|
}));
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
conversationId:
|
|
42
|
-
messageId:
|
|
41
|
+
function defaultPairing(): PairingResult {
|
|
42
|
+
return {
|
|
43
|
+
conversationId: null,
|
|
44
|
+
messageId: null,
|
|
43
45
|
strategy: "start_new_conversation",
|
|
44
46
|
createdNewConversation: false,
|
|
45
47
|
conversationFallbackUsed: false,
|
|
46
|
-
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let pairingByChannel: Record<string, PairingResult> = {};
|
|
52
|
+
let pairingErrorByChannel: Record<string, Error> = {};
|
|
53
|
+
|
|
54
|
+
mock.module("../conversation-pairing.js", () => ({
|
|
55
|
+
pairDeliveryWithConversation: async (_signal: unknown, channel: string) => {
|
|
56
|
+
const error = pairingErrorByChannel[channel];
|
|
57
|
+
if (error) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
return pairingByChannel[channel] ?? defaultPairing();
|
|
61
|
+
},
|
|
47
62
|
}));
|
|
48
63
|
|
|
49
64
|
mock.module("../deliveries-store.js", () => ({
|
|
@@ -121,7 +136,10 @@ interface CapturedSend {
|
|
|
121
136
|
destination: ChannelDestination;
|
|
122
137
|
}
|
|
123
138
|
|
|
124
|
-
function makeCapturingAdapter(
|
|
139
|
+
function makeCapturingAdapter(
|
|
140
|
+
channel: "vellum" | "platform",
|
|
141
|
+
result: DeliveryResult = { success: true },
|
|
142
|
+
): {
|
|
125
143
|
adapter: ChannelAdapter;
|
|
126
144
|
sends: CapturedSend[];
|
|
127
145
|
} {
|
|
@@ -133,7 +151,7 @@ function makeCapturingAdapter(channel: "vellum" | "platform"): {
|
|
|
133
151
|
destination: ChannelDestination,
|
|
134
152
|
): Promise<DeliveryResult> {
|
|
135
153
|
sends.push({ payload, destination });
|
|
136
|
-
return
|
|
154
|
+
return result;
|
|
137
155
|
},
|
|
138
156
|
};
|
|
139
157
|
return { adapter, sends };
|
|
@@ -142,6 +160,8 @@ function makeCapturingAdapter(channel: "vellum" | "platform"): {
|
|
|
142
160
|
beforeEach(() => {
|
|
143
161
|
composeFallbackReturn = {};
|
|
144
162
|
knownConversations = new Set();
|
|
163
|
+
pairingByChannel = {};
|
|
164
|
+
pairingErrorByChannel = {};
|
|
145
165
|
});
|
|
146
166
|
|
|
147
167
|
// ── Tests ───────────────────────────────────────────────────────────────
|
|
@@ -293,6 +313,326 @@ describe("NotificationBroadcaster platform deep-link from contextPayload", () =>
|
|
|
293
313
|
});
|
|
294
314
|
});
|
|
295
315
|
|
|
316
|
+
describe("NotificationBroadcaster remotePushDispatched flag", () => {
|
|
317
|
+
const bothChannelsCopy = () => {
|
|
318
|
+
composeFallbackReturn = {
|
|
319
|
+
vellum: { title: "Reminder", body: "Hello" },
|
|
320
|
+
platform: { title: "Reminder", body: "Hello" },
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const bothChannelsDecision = () =>
|
|
325
|
+
makeDecision({
|
|
326
|
+
selectedChannels: ["vellum", "platform"],
|
|
327
|
+
renderedCopy: {},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("vellum payload carries true when the platform adapter reports an accepted push; platform payload omits it", async () => {
|
|
331
|
+
bothChannelsCopy();
|
|
332
|
+
|
|
333
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
334
|
+
const platform = makeCapturingAdapter("platform", {
|
|
335
|
+
success: true,
|
|
336
|
+
remotePushAccepted: true,
|
|
337
|
+
});
|
|
338
|
+
const broadcaster = new NotificationBroadcaster([
|
|
339
|
+
vellum.adapter,
|
|
340
|
+
platform.adapter,
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
|
|
344
|
+
|
|
345
|
+
expect(vellum.sends.length).toBe(1);
|
|
346
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(true);
|
|
347
|
+
expect(platform.sends.length).toBe(1);
|
|
348
|
+
expect(platform.sends[0]?.payload.correlationId).toBe(
|
|
349
|
+
vellum.sends[0]?.payload.correlationId,
|
|
350
|
+
);
|
|
351
|
+
expect(platform.sends[0]?.payload.remotePushDispatched).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("vellum payload carries false when the platform dispatch fails", async () => {
|
|
355
|
+
bothChannelsCopy();
|
|
356
|
+
|
|
357
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
358
|
+
const platform = makeCapturingAdapter("platform", {
|
|
359
|
+
success: false,
|
|
360
|
+
error: "HTTP 503",
|
|
361
|
+
});
|
|
362
|
+
const broadcaster = new NotificationBroadcaster([
|
|
363
|
+
vellum.adapter,
|
|
364
|
+
platform.adapter,
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
|
|
368
|
+
|
|
369
|
+
expect(vellum.sends.length).toBe(1);
|
|
370
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("vellum payload carries platforms accepted before a partial failure", async () => {
|
|
374
|
+
bothChannelsCopy();
|
|
375
|
+
|
|
376
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
377
|
+
const platform = makeCapturingAdapter("platform", {
|
|
378
|
+
success: false,
|
|
379
|
+
error: "FCM failed",
|
|
380
|
+
remotePushPlatforms: ["ios"],
|
|
381
|
+
});
|
|
382
|
+
const broadcaster = new NotificationBroadcaster([
|
|
383
|
+
vellum.adapter,
|
|
384
|
+
platform.adapter,
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
|
|
388
|
+
|
|
389
|
+
expect(vellum.sends[0]?.payload.remotePushPlatforms).toEqual(["ios"]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("vellum payload carries false when the platform reports no accepted push (skipped or zero tokens)", async () => {
|
|
393
|
+
bothChannelsCopy();
|
|
394
|
+
|
|
395
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
396
|
+
const platform = makeCapturingAdapter("platform", {
|
|
397
|
+
success: true,
|
|
398
|
+
remotePushAccepted: false,
|
|
399
|
+
});
|
|
400
|
+
const broadcaster = new NotificationBroadcaster([
|
|
401
|
+
vellum.adapter,
|
|
402
|
+
platform.adapter,
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
|
|
406
|
+
|
|
407
|
+
expect(vellum.sends.length).toBe(1);
|
|
408
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("vellum payload carries false when the platform result omits remotePushAccepted", async () => {
|
|
412
|
+
bothChannelsCopy();
|
|
413
|
+
|
|
414
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
415
|
+
const platform = makeCapturingAdapter("platform", { success: true });
|
|
416
|
+
const broadcaster = new NotificationBroadcaster([
|
|
417
|
+
vellum.adapter,
|
|
418
|
+
platform.adapter,
|
|
419
|
+
]);
|
|
420
|
+
|
|
421
|
+
await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
|
|
422
|
+
|
|
423
|
+
expect(vellum.sends.length).toBe(1);
|
|
424
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("vellum payload carries false when the platform adapter throws", async () => {
|
|
428
|
+
bothChannelsCopy();
|
|
429
|
+
|
|
430
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
431
|
+
const platform: ChannelAdapter = {
|
|
432
|
+
channel: "platform",
|
|
433
|
+
async send(): Promise<DeliveryResult> {
|
|
434
|
+
throw new Error("boom");
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
const broadcaster = new NotificationBroadcaster([vellum.adapter, platform]);
|
|
438
|
+
|
|
439
|
+
const results = await broadcaster.broadcastDecision(
|
|
440
|
+
makeSignal(),
|
|
441
|
+
bothChannelsDecision(),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
expect(vellum.sends.length).toBe(1);
|
|
445
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
|
|
446
|
+
expect(results.find((r) => r.channel === "platform")?.status).toBe(
|
|
447
|
+
"failed",
|
|
448
|
+
);
|
|
449
|
+
expect(results.find((r) => r.channel === "vellum")?.status).toBe("sent");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("vellum intent still flushes with false when the platform channel's prep throws", async () => {
|
|
453
|
+
bothChannelsCopy();
|
|
454
|
+
pairingErrorByChannel = { platform: new Error("pairing exploded") };
|
|
455
|
+
|
|
456
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
457
|
+
const platform = makeCapturingAdapter("platform", {
|
|
458
|
+
success: true,
|
|
459
|
+
remotePushAccepted: true,
|
|
460
|
+
});
|
|
461
|
+
const broadcaster = new NotificationBroadcaster([
|
|
462
|
+
vellum.adapter,
|
|
463
|
+
platform.adapter,
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
await expect(
|
|
467
|
+
broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision()),
|
|
468
|
+
).rejects.toThrow("pairing exploded");
|
|
469
|
+
|
|
470
|
+
// The platform adapter never ran, but the deferred vellum send must not
|
|
471
|
+
// be lost -- its pending delivery row would block retries forever.
|
|
472
|
+
expect(platform.sends.length).toBe(0);
|
|
473
|
+
expect(vellum.sends.length).toBe(1);
|
|
474
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("deadline expiry flushes vellum with false while the platform dispatch finishes in the background", async () => {
|
|
478
|
+
bothChannelsCopy();
|
|
479
|
+
|
|
480
|
+
let resolvePlatform: ((result: DeliveryResult) => void) | undefined;
|
|
481
|
+
const platformSends: CapturedSend[] = [];
|
|
482
|
+
const platform: ChannelAdapter = {
|
|
483
|
+
channel: "platform",
|
|
484
|
+
send(
|
|
485
|
+
payload: ChannelDeliveryPayload,
|
|
486
|
+
destination: ChannelDestination,
|
|
487
|
+
observer?: ChannelDeliveryObserver,
|
|
488
|
+
): Promise<DeliveryResult> {
|
|
489
|
+
platformSends.push({ payload, destination });
|
|
490
|
+
observer?.onRemotePushPlatforms(["ios"]);
|
|
491
|
+
return new Promise<DeliveryResult>((resolve) => {
|
|
492
|
+
resolvePlatform = resolve;
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
497
|
+
const broadcaster = new NotificationBroadcaster([vellum.adapter, platform]);
|
|
498
|
+
|
|
499
|
+
const results = await broadcaster.broadcastDecision(
|
|
500
|
+
makeSignal(),
|
|
501
|
+
bothChannelsDecision(),
|
|
502
|
+
{ platformOutcomeDeadlineMs: 20 },
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
expect(platformSends.length).toBe(1);
|
|
506
|
+
expect(vellum.sends.length).toBe(1);
|
|
507
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
|
|
508
|
+
expect(vellum.sends[0]?.payload.remotePushPlatforms).toEqual(["ios"]);
|
|
509
|
+
expect(results.find((r) => r.channel === "platform")?.status).toBe(
|
|
510
|
+
"pending",
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
// The dispatch settling after the deadline must not re-send vellum.
|
|
514
|
+
resolvePlatform?.({ success: true, remotePushAccepted: true });
|
|
515
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
516
|
+
expect(vellum.sends.length).toBe(1);
|
|
517
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("platform completing before the deadline sets the real outcome on the vellum payload", async () => {
|
|
521
|
+
bothChannelsCopy();
|
|
522
|
+
|
|
523
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
524
|
+
const platform: ChannelAdapter = {
|
|
525
|
+
channel: "platform",
|
|
526
|
+
async send(): Promise<DeliveryResult> {
|
|
527
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
528
|
+
return { success: true, remotePushAccepted: true };
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
const broadcaster = new NotificationBroadcaster([vellum.adapter, platform]);
|
|
532
|
+
|
|
533
|
+
const results = await broadcaster.broadcastDecision(
|
|
534
|
+
makeSignal(),
|
|
535
|
+
bothChannelsDecision(),
|
|
536
|
+
{ platformOutcomeDeadlineMs: 1_000 },
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(vellum.sends.length).toBe(1);
|
|
540
|
+
expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(true);
|
|
541
|
+
expect(results.find((r) => r.channel === "platform")?.status).toBe("sent");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("dispatches the platform adapter before the vellum intent when both are selected", async () => {
|
|
545
|
+
bothChannelsCopy();
|
|
546
|
+
|
|
547
|
+
const callOrder: string[] = [];
|
|
548
|
+
const vellum: ChannelAdapter = {
|
|
549
|
+
channel: "vellum",
|
|
550
|
+
async send(): Promise<DeliveryResult> {
|
|
551
|
+
callOrder.push("vellum");
|
|
552
|
+
return { success: true };
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
const platform: ChannelAdapter = {
|
|
556
|
+
channel: "platform",
|
|
557
|
+
async send(): Promise<DeliveryResult> {
|
|
558
|
+
callOrder.push("platform");
|
|
559
|
+
return { success: true, remotePushAccepted: true };
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
const broadcaster = new NotificationBroadcaster([vellum, platform]);
|
|
563
|
+
|
|
564
|
+
await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
|
|
565
|
+
|
|
566
|
+
expect(callOrder).toEqual(["platform", "vellum"]);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("platform deep link still carries the vellum pairing when the vellum send is deferred", async () => {
|
|
570
|
+
bothChannelsCopy();
|
|
571
|
+
|
|
572
|
+
pairingByChannel = {
|
|
573
|
+
vellum: {
|
|
574
|
+
conversationId: "conv-vellum-1",
|
|
575
|
+
messageId: "msg-1",
|
|
576
|
+
strategy: "start_new_conversation",
|
|
577
|
+
createdNewConversation: false,
|
|
578
|
+
conversationFallbackUsed: false,
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
583
|
+
const platform = makeCapturingAdapter("platform", {
|
|
584
|
+
success: true,
|
|
585
|
+
remotePushAccepted: true,
|
|
586
|
+
});
|
|
587
|
+
const broadcaster = new NotificationBroadcaster([
|
|
588
|
+
vellum.adapter,
|
|
589
|
+
platform.adapter,
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
|
|
593
|
+
|
|
594
|
+
expect(platform.sends.length).toBe(1);
|
|
595
|
+
expect(platform.sends[0]?.payload.deepLinkTarget).toEqual({
|
|
596
|
+
conversationId: "conv-vellum-1",
|
|
597
|
+
messageId: "msg-1",
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe("NotificationBroadcaster forced-platform copy reuse", () => {
|
|
603
|
+
test("platform payload reuses the vellum rendered copy instead of template fallback", async () => {
|
|
604
|
+
// A urgency-forced platform channel has no rendered copy of its own; the
|
|
605
|
+
// template fallback must lose to the vellum channel's rendered copy.
|
|
606
|
+
composeFallbackReturn = {
|
|
607
|
+
platform: { title: "Template title", body: "Template body" },
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const vellum = makeCapturingAdapter("vellum");
|
|
611
|
+
const platform = makeCapturingAdapter("platform", {
|
|
612
|
+
success: true,
|
|
613
|
+
remotePushAccepted: true,
|
|
614
|
+
});
|
|
615
|
+
const broadcaster = new NotificationBroadcaster([
|
|
616
|
+
vellum.adapter,
|
|
617
|
+
platform.adapter,
|
|
618
|
+
]);
|
|
619
|
+
|
|
620
|
+
await broadcaster.broadcastDecision(
|
|
621
|
+
makeSignal(),
|
|
622
|
+
makeDecision({
|
|
623
|
+
selectedChannels: ["vellum", "platform"],
|
|
624
|
+
renderedCopy: {
|
|
625
|
+
vellum: { title: "Rendered title", body: "Rendered body" },
|
|
626
|
+
},
|
|
627
|
+
}),
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
expect(platform.sends.length).toBe(1);
|
|
631
|
+
expect(platform.sends[0]?.payload.copy.title).toBe("Rendered title");
|
|
632
|
+
expect(platform.sends[0]?.payload.copy.body).toBe("Rendered body");
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
296
636
|
// The card context is built once per broadcast (adapters render only); an
|
|
297
637
|
// answer-mode pending_question with structured options renders them as
|
|
298
638
|
// tappable card actions in the answer-token scheme the reply router
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the notification copy-composer
|
|
3
|
-
*
|
|
4
|
-
* the shared deriveTitle
|
|
2
|
+
* Tests for the notification copy-composer: the fallback path that
|
|
3
|
+
* composeFallbackCopy uses when the LLM is unavailable, the normalization
|
|
4
|
+
* it applies to producer-supplied titles, and the shared deriveTitle
|
|
5
|
+
* utility.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { describe, expect, test } from "bun:test";
|
|
@@ -190,6 +191,55 @@ describe("composeFallbackCopy honors requestedMessage / requestedTitle", () => {
|
|
|
190
191
|
expect(copy.vellum?.title).toBe("User Title");
|
|
191
192
|
});
|
|
192
193
|
|
|
194
|
+
test("strips markdown and quotes from a producer-supplied title", () => {
|
|
195
|
+
const signal = makeSignal({
|
|
196
|
+
contextPayload: {
|
|
197
|
+
requestedMessage: "The deploy finished cleanly.",
|
|
198
|
+
requestedTitle: '"**Deploy** finished"',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const copy = composeFallbackCopy(signal, CHANNELS);
|
|
202
|
+
|
|
203
|
+
expect(copy.vellum?.title).toBe("Deploy finished");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("derives the title when requestedTitle reads as leaked prose", () => {
|
|
207
|
+
const signal = makeSignal({
|
|
208
|
+
contextPayload: {
|
|
209
|
+
requestedMessage: "Backup finished. Nothing needs your attention.",
|
|
210
|
+
requestedTitle: "I need to generate a title for this notification",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
const copy = composeFallbackCopy(signal, CHANNELS);
|
|
214
|
+
|
|
215
|
+
expect(copy.vellum?.title).toBe("Backup finished.");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("derives the title when requestedTitle spans multiple lines", () => {
|
|
219
|
+
const signal = makeSignal({
|
|
220
|
+
contextPayload: {
|
|
221
|
+
requestedMessage: "The nightly job is done.",
|
|
222
|
+
requestedTitle: "Nightly job\nsecond line",
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
const copy = composeFallbackCopy(signal, CHANNELS);
|
|
226
|
+
|
|
227
|
+
expect(copy.vellum?.title).toBe("The nightly job is done.");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("truncates an over-long requestedTitle to the shared title budget", () => {
|
|
231
|
+
const signal = makeSignal({
|
|
232
|
+
contextPayload: {
|
|
233
|
+
requestedMessage: "Body text",
|
|
234
|
+
requestedTitle:
|
|
235
|
+
"Alpha bravo charlie delta echo foxtrot golf hotel india juliett",
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const copy = composeFallbackCopy(signal, CHANNELS);
|
|
239
|
+
|
|
240
|
+
expect(copy.vellum?.title).toBe("Alpha bravo charlie delta echo");
|
|
241
|
+
});
|
|
242
|
+
|
|
193
243
|
test("works with non-assistant_tool source channels", () => {
|
|
194
244
|
for (const sourceChannel of ["scheduler", "watcher", "slack"] as const) {
|
|
195
245
|
const signal = makeSignal({
|