@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,6 +7,7 @@ const runDeterministicChecksMock = mock();
|
|
|
7
7
|
const createEventMock = mock();
|
|
8
8
|
const updateEventDedupeKeyMock = mock();
|
|
9
9
|
const dispatchDecisionMock = mock();
|
|
10
|
+
const isPlatformClientConfiguredMock = mock();
|
|
10
11
|
|
|
11
12
|
mock.module("../channels/config.js", () => ({
|
|
12
13
|
getDeliverableChannels: () => ["vellum", "telegram"],
|
|
@@ -38,6 +39,12 @@ mock.module("../notifications/broadcaster.js", () => ({
|
|
|
38
39
|
},
|
|
39
40
|
}));
|
|
40
41
|
|
|
42
|
+
// Capture the real enforcement before the module mock below replaces it:
|
|
43
|
+
// the single_channel interplay tests exercise the genuine cap logic against
|
|
44
|
+
// the urgency force-add's channel ordering.
|
|
45
|
+
const { enforceRoutingIntent: realEnforceRoutingIntent } =
|
|
46
|
+
await import("../notifications/decision-engine.js");
|
|
47
|
+
|
|
41
48
|
mock.module("../notifications/decision-engine.js", () => ({
|
|
42
49
|
evaluateSignal: (...args: unknown[]) => evaluateSignalMock(...args),
|
|
43
50
|
enforceRoutingIntent: (...args: unknown[]) =>
|
|
@@ -78,27 +85,41 @@ mock.module("../notifications/runtime-dispatch.js", () => ({
|
|
|
78
85
|
dispatchDecision: (...args: unknown[]) => dispatchDecisionMock(...args),
|
|
79
86
|
}));
|
|
80
87
|
|
|
88
|
+
mock.module("../platform/client.js", () => ({
|
|
89
|
+
// The adapter import graph needs the class symbol; nothing in these tests
|
|
90
|
+
// dispatches through it.
|
|
91
|
+
VellumPlatformClient: class {
|
|
92
|
+
static async create(): Promise<null> {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
isPlatformClientConfigured: () => isPlatformClientConfiguredMock(),
|
|
97
|
+
}));
|
|
98
|
+
|
|
81
99
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
evaluateSignalMock.mockReset();
|
|
103
|
+
enforceRoutingIntentMock.mockReset();
|
|
104
|
+
updateDecisionMock.mockReset();
|
|
105
|
+
runDeterministicChecksMock.mockReset();
|
|
106
|
+
createEventMock.mockReset();
|
|
107
|
+
updateEventDedupeKeyMock.mockReset();
|
|
108
|
+
dispatchDecisionMock.mockReset();
|
|
109
|
+
isPlatformClientConfiguredMock.mockReset();
|
|
110
|
+
|
|
111
|
+
createEventMock.mockReturnValue({ id: "evt-1" });
|
|
112
|
+
runDeterministicChecksMock.mockResolvedValue({ passed: true });
|
|
113
|
+
dispatchDecisionMock.mockResolvedValue({
|
|
114
|
+
dispatched: true,
|
|
115
|
+
reason: "ok",
|
|
116
|
+
deliveryResults: [],
|
|
100
117
|
});
|
|
118
|
+
isPlatformClientConfiguredMock.mockResolvedValue(true);
|
|
119
|
+
enforceRoutingIntentMock.mockImplementation((decision: unknown) => decision);
|
|
120
|
+
});
|
|
101
121
|
|
|
122
|
+
describe("emitNotificationSignal routing intent re-persistence", () => {
|
|
102
123
|
test("re-persists selectedChannels/reasoningSummary when enforcement changes the decision", async () => {
|
|
103
124
|
const preDecision = {
|
|
104
125
|
shouldNotify: true,
|
|
@@ -226,21 +247,6 @@ describe("emitNotificationSignal routing intent re-persistence", () => {
|
|
|
226
247
|
});
|
|
227
248
|
|
|
228
249
|
describe("emitNotificationSignal source-active pre-gate", () => {
|
|
229
|
-
beforeEach(() => {
|
|
230
|
-
evaluateSignalMock.mockReset();
|
|
231
|
-
runDeterministicChecksMock.mockReset();
|
|
232
|
-
createEventMock.mockReset();
|
|
233
|
-
dispatchDecisionMock.mockReset();
|
|
234
|
-
|
|
235
|
-
createEventMock.mockReturnValue({ id: "evt-src-active" });
|
|
236
|
-
runDeterministicChecksMock.mockResolvedValue({ passed: true });
|
|
237
|
-
dispatchDecisionMock.mockResolvedValue({
|
|
238
|
-
dispatched: true,
|
|
239
|
-
reason: "ok",
|
|
240
|
-
deliveryResults: [],
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
250
|
test("suppresses visibleInSourceNow signals before the decision engine runs", async () => {
|
|
245
251
|
const result = await emitNotificationSignal({
|
|
246
252
|
sourceEventName: "ingress.trusted_contact.verification_sent",
|
|
@@ -294,23 +300,6 @@ describe("emitNotificationSignal source-active pre-gate", () => {
|
|
|
294
300
|
});
|
|
295
301
|
|
|
296
302
|
describe("access-request vellum floor", () => {
|
|
297
|
-
beforeEach(() => {
|
|
298
|
-
evaluateSignalMock.mockReset();
|
|
299
|
-
enforceRoutingIntentMock.mockReset();
|
|
300
|
-
updateDecisionMock.mockReset();
|
|
301
|
-
runDeterministicChecksMock.mockReset();
|
|
302
|
-
createEventMock.mockReset();
|
|
303
|
-
updateEventDedupeKeyMock.mockReset();
|
|
304
|
-
dispatchDecisionMock.mockReset();
|
|
305
|
-
createEventMock.mockReturnValue({ id: "evt-1" });
|
|
306
|
-
runDeterministicChecksMock.mockResolvedValue({ passed: true });
|
|
307
|
-
dispatchDecisionMock.mockResolvedValue({
|
|
308
|
-
dispatched: true,
|
|
309
|
-
reason: "ok",
|
|
310
|
-
deliveryResults: [],
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
303
|
test("rescues a suppressed access-request decision onto the vellum channel", async () => {
|
|
315
304
|
evaluateSignalMock.mockResolvedValue({
|
|
316
305
|
shouldNotify: false,
|
|
@@ -467,3 +456,255 @@ describe("access-request vellum floor", () => {
|
|
|
467
456
|
expect(dispatched.reasoningSummary).not.toContain("vellum forced");
|
|
468
457
|
});
|
|
469
458
|
});
|
|
459
|
+
|
|
460
|
+
describe("high/critical urgency channel force", () => {
|
|
461
|
+
function makeDecision(overrides: Record<string, unknown>) {
|
|
462
|
+
return {
|
|
463
|
+
shouldNotify: true,
|
|
464
|
+
selectedChannels: ["telegram"],
|
|
465
|
+
reasoningSummary: "LLM selected telegram only",
|
|
466
|
+
renderedCopy: {},
|
|
467
|
+
dedupeKey: "dedupe-urg-1",
|
|
468
|
+
confidence: 0.9,
|
|
469
|
+
fallbackUsed: false,
|
|
470
|
+
persistedDecisionId: "dec-urg-1",
|
|
471
|
+
...overrides,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function emitWithUrgency(urgency: "low" | "medium" | "high" | "critical") {
|
|
476
|
+
return emitNotificationSignal({
|
|
477
|
+
sourceEventName: "schedule.notify",
|
|
478
|
+
sourceChannel: "scheduler",
|
|
479
|
+
sourceContextId: "rem-urg-1",
|
|
480
|
+
attentionHints: {
|
|
481
|
+
requiresAction: true,
|
|
482
|
+
urgency,
|
|
483
|
+
isAsyncBackground: false,
|
|
484
|
+
visibleInSourceNow: false,
|
|
485
|
+
},
|
|
486
|
+
contextPayload: { reminderId: "rem-urg-1" },
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
test("forces vellum and platform onto a high-urgency decision missing both", async () => {
|
|
491
|
+
evaluateSignalMock.mockResolvedValue(makeDecision({}));
|
|
492
|
+
|
|
493
|
+
const result = await emitWithUrgency("high");
|
|
494
|
+
|
|
495
|
+
expect(result.dispatched).toBe(true);
|
|
496
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
497
|
+
selectedChannels: string[];
|
|
498
|
+
reasoningSummary: string;
|
|
499
|
+
};
|
|
500
|
+
expect(dispatched.selectedChannels).toContain("vellum");
|
|
501
|
+
expect(dispatched.selectedChannels).toContain("platform");
|
|
502
|
+
expect(dispatched.selectedChannels).toContain("telegram");
|
|
503
|
+
expect(dispatched.reasoningSummary).toContain(
|
|
504
|
+
"(vellum, platform forced: high urgency)",
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("re-persists the stored decision with the force-added channels when routing enforcement is a no-op", async () => {
|
|
509
|
+
evaluateSignalMock.mockResolvedValue(makeDecision({}));
|
|
510
|
+
|
|
511
|
+
await emitWithUrgency("high");
|
|
512
|
+
|
|
513
|
+
// The identity enforcement mock leaves the decision unchanged, so only
|
|
514
|
+
// the urgency force-add differs from the evaluated decision. The stored
|
|
515
|
+
// row must still be synced to the dispatched channels.
|
|
516
|
+
expect(updateDecisionMock).toHaveBeenCalledTimes(1);
|
|
517
|
+
expect(updateDecisionMock).toHaveBeenCalledWith("dec-urg-1", {
|
|
518
|
+
selectedChannels: ["vellum", "telegram", "platform"],
|
|
519
|
+
reasoningSummary:
|
|
520
|
+
"LLM selected telegram only (vellum, platform forced: high urgency)",
|
|
521
|
+
validationResults: {
|
|
522
|
+
dedupeKey: "dedupe-urg-1",
|
|
523
|
+
channelCount: 3,
|
|
524
|
+
hasCopy: false,
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("forces only the missing channel when the other is already selected", async () => {
|
|
530
|
+
evaluateSignalMock.mockResolvedValue(
|
|
531
|
+
makeDecision({ selectedChannels: ["vellum"] }),
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
await emitWithUrgency("critical");
|
|
535
|
+
|
|
536
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
537
|
+
selectedChannels: string[];
|
|
538
|
+
reasoningSummary: string;
|
|
539
|
+
};
|
|
540
|
+
expect(dispatched.selectedChannels).toEqual(["vellum", "platform"]);
|
|
541
|
+
expect(dispatched.reasoningSummary).toContain(
|
|
542
|
+
"(platform forced: critical urgency)",
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("leaves a high-urgency decision untouched when both channels are already selected", async () => {
|
|
547
|
+
evaluateSignalMock.mockResolvedValue(
|
|
548
|
+
makeDecision({ selectedChannels: ["vellum", "platform"] }),
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
await emitWithUrgency("high");
|
|
552
|
+
|
|
553
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
554
|
+
selectedChannels: string[];
|
|
555
|
+
reasoningSummary: string;
|
|
556
|
+
};
|
|
557
|
+
expect(dispatched.selectedChannels).toEqual(["vellum", "platform"]);
|
|
558
|
+
expect(dispatched.reasoningSummary).not.toContain("forced");
|
|
559
|
+
expect(updateDecisionMock).not.toHaveBeenCalled();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("leaves medium and low urgency selections untouched", async () => {
|
|
563
|
+
for (const urgency of ["medium", "low"] as const) {
|
|
564
|
+
dispatchDecisionMock.mockClear();
|
|
565
|
+
evaluateSignalMock.mockResolvedValue(makeDecision({}));
|
|
566
|
+
|
|
567
|
+
await emitWithUrgency(urgency);
|
|
568
|
+
|
|
569
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
570
|
+
selectedChannels: string[];
|
|
571
|
+
reasoningSummary: string;
|
|
572
|
+
};
|
|
573
|
+
expect(dispatched.selectedChannels).toEqual(["telegram"]);
|
|
574
|
+
expect(dispatched.reasoningSummary).not.toContain("forced");
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("does not force channels when the decision suppresses the notification", async () => {
|
|
579
|
+
evaluateSignalMock.mockResolvedValue(
|
|
580
|
+
makeDecision({ shouldNotify: false, selectedChannels: [] }),
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
await emitWithUrgency("high");
|
|
584
|
+
|
|
585
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
586
|
+
shouldNotify: boolean;
|
|
587
|
+
selectedChannels: string[];
|
|
588
|
+
reasoningSummary: string;
|
|
589
|
+
};
|
|
590
|
+
expect(dispatched.shouldNotify).toBe(false);
|
|
591
|
+
expect(dispatched.selectedChannels).toEqual([]);
|
|
592
|
+
expect(dispatched.reasoningSummary).not.toContain("forced");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("routing-intent enforcement runs after the force-add and can cap channels", async () => {
|
|
596
|
+
evaluateSignalMock.mockResolvedValue(makeDecision({}));
|
|
597
|
+
// single_channel enforcement caps the selection to the source channel.
|
|
598
|
+
enforceRoutingIntentMock.mockImplementation(
|
|
599
|
+
(decision: { selectedChannels: string[] }) => ({
|
|
600
|
+
...decision,
|
|
601
|
+
selectedChannels: ["telegram"],
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
await emitNotificationSignal({
|
|
606
|
+
sourceEventName: "schedule.notify",
|
|
607
|
+
sourceChannel: "scheduler",
|
|
608
|
+
sourceContextId: "rem-urg-2",
|
|
609
|
+
routingIntent: "single_channel",
|
|
610
|
+
attentionHints: {
|
|
611
|
+
requiresAction: true,
|
|
612
|
+
urgency: "high",
|
|
613
|
+
isAsyncBackground: false,
|
|
614
|
+
visibleInSourceNow: false,
|
|
615
|
+
},
|
|
616
|
+
contextPayload: { reminderId: "rem-urg-2" },
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Enforcement sees the force-added channels...
|
|
620
|
+
const enforcedInput = enforceRoutingIntentMock.mock.calls[0][0] as {
|
|
621
|
+
selectedChannels: string[];
|
|
622
|
+
};
|
|
623
|
+
expect(enforcedInput.selectedChannels).toEqual([
|
|
624
|
+
"vellum",
|
|
625
|
+
"telegram",
|
|
626
|
+
"platform",
|
|
627
|
+
]);
|
|
628
|
+
// ...and its cap wins over the force-add.
|
|
629
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
630
|
+
selectedChannels: string[];
|
|
631
|
+
};
|
|
632
|
+
expect(dispatched.selectedChannels).toEqual(["telegram"]);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("forces only vellum when the platform client is not configured", async () => {
|
|
636
|
+
isPlatformClientConfiguredMock.mockResolvedValue(false);
|
|
637
|
+
evaluateSignalMock.mockResolvedValue(makeDecision({}));
|
|
638
|
+
|
|
639
|
+
await emitWithUrgency("high");
|
|
640
|
+
|
|
641
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
642
|
+
selectedChannels: string[];
|
|
643
|
+
reasoningSummary: string;
|
|
644
|
+
};
|
|
645
|
+
expect(dispatched.selectedChannels).toEqual(["vellum", "telegram"]);
|
|
646
|
+
expect(dispatched.reasoningSummary).toContain(
|
|
647
|
+
"(vellum forced: high urgency)",
|
|
648
|
+
);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Real single_channel enforcement caps to the source channel when it is
|
|
652
|
+
// connected, else to the FIRST selected channel. The force-add prepends
|
|
653
|
+
// vellum precisely so that fallback keeps the in-app banner; these tests
|
|
654
|
+
// run the genuine enforcement to pin the interplay.
|
|
655
|
+
describe("interplay with real single_channel enforcement", () => {
|
|
656
|
+
beforeEach(() => {
|
|
657
|
+
enforceRoutingIntentMock.mockImplementation(
|
|
658
|
+
realEnforceRoutingIntent as (...args: unknown[]) => unknown,
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
function emitHighUrgencySingleChannel(
|
|
663
|
+
sourceChannel: "watcher" | "assistant_tool",
|
|
664
|
+
) {
|
|
665
|
+
return emitNotificationSignal({
|
|
666
|
+
sourceEventName: "watcher.escalation",
|
|
667
|
+
sourceChannel,
|
|
668
|
+
sourceContextId: "watch-1",
|
|
669
|
+
routingIntent: "single_channel",
|
|
670
|
+
attentionHints: {
|
|
671
|
+
requiresAction: true,
|
|
672
|
+
urgency: "high",
|
|
673
|
+
isAsyncBackground: false,
|
|
674
|
+
visibleInSourceNow: false,
|
|
675
|
+
},
|
|
676
|
+
contextPayload: {},
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
test("vellum survives the cap when the decision selected only vellum", async () => {
|
|
681
|
+
evaluateSignalMock.mockResolvedValue(
|
|
682
|
+
makeDecision({ selectedChannels: ["vellum"] }),
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
await emitHighUrgencySingleChannel("watcher");
|
|
686
|
+
|
|
687
|
+
// Force-add appends platform: ["vellum", "platform"]. The source
|
|
688
|
+
// channel is not connected, so the cap falls back to index 0.
|
|
689
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
690
|
+
selectedChannels: string[];
|
|
691
|
+
};
|
|
692
|
+
expect(dispatched.selectedChannels).toEqual(["vellum"]);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("the cap picks the prepended vellum when the decision selected another channel", async () => {
|
|
696
|
+
evaluateSignalMock.mockResolvedValue(
|
|
697
|
+
makeDecision({ selectedChannels: ["telegram"] }),
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
await emitHighUrgencySingleChannel("assistant_tool");
|
|
701
|
+
|
|
702
|
+
// Force-add yields ["vellum", "telegram", "platform"]; the
|
|
703
|
+
// non-connected source channel makes the cap take index 0.
|
|
704
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
705
|
+
selectedChannels: string[];
|
|
706
|
+
};
|
|
707
|
+
expect(dispatched.selectedChannels).toEqual(["vellum"]);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
});
|
|
@@ -14,6 +14,13 @@ mock.module("../messaging/providers/slack/withdraw.js", () => ({
|
|
|
14
14
|
withdrawSlackApprovalCard,
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
+
const withdrawTelegramApprovalCard = mock(
|
|
18
|
+
async (_params: Record<string, unknown>) => {},
|
|
19
|
+
);
|
|
20
|
+
mock.module("../messaging/providers/telegram-bot/withdraw.js", () => ({
|
|
21
|
+
withdrawTelegramApprovalCard,
|
|
22
|
+
}));
|
|
23
|
+
|
|
17
24
|
// The recorder writes through the gateway client; serve that surface from
|
|
18
25
|
// the in-memory sim the assertions read.
|
|
19
26
|
import {
|
|
@@ -60,6 +67,7 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
60
67
|
completeSurfaceAndNotify.mockClear();
|
|
61
68
|
markSurfaceCompleted.mockClear();
|
|
62
69
|
withdrawSlackApprovalCard.mockClear();
|
|
70
|
+
withdrawTelegramApprovalCard.mockClear();
|
|
63
71
|
});
|
|
64
72
|
|
|
65
73
|
test("withdraws + broadcasts the in-app card when the decision came from another surface", async () => {
|
|
@@ -199,7 +207,7 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
199
207
|
expect(withdrawSlackApprovalCard).toHaveBeenCalledTimes(1);
|
|
200
208
|
});
|
|
201
209
|
|
|
202
|
-
test("
|
|
210
|
+
test("withdraws the Telegram card with a status reply when resolved from another surface", async () => {
|
|
203
211
|
const req = makeRequest({ sourceChannel: "telegram" });
|
|
204
212
|
bridgeState.seedDelivery({
|
|
205
213
|
requestId: req.id,
|
|
@@ -208,10 +216,90 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
208
216
|
destinationMessageId: "9",
|
|
209
217
|
});
|
|
210
218
|
|
|
211
|
-
await withdrawGuardianRequestCards({
|
|
219
|
+
await withdrawGuardianRequestCards({
|
|
220
|
+
request: req,
|
|
221
|
+
status: "approved",
|
|
222
|
+
originChannel: "vellum",
|
|
223
|
+
});
|
|
212
224
|
|
|
213
225
|
expect(withdrawSlackApprovalCard).not.toHaveBeenCalled();
|
|
214
|
-
expect(
|
|
226
|
+
expect(withdrawTelegramApprovalCard).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(withdrawTelegramApprovalCard).toHaveBeenCalledWith({
|
|
228
|
+
chatId: "T1",
|
|
229
|
+
messageId: "9",
|
|
230
|
+
status: "approved",
|
|
231
|
+
postStatusReply: true,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("suppresses the Telegram status reply only when the origin flow replies to the guardian", async () => {
|
|
236
|
+
const req = makeRequest({ sourceChannel: "telegram" });
|
|
237
|
+
bridgeState.seedDelivery({
|
|
238
|
+
requestId: req.id,
|
|
239
|
+
destinationChannel: "telegram",
|
|
240
|
+
destinationChatId: "T1",
|
|
241
|
+
destinationMessageId: "9",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await withdrawGuardianRequestCards({
|
|
245
|
+
request: req,
|
|
246
|
+
status: "denied",
|
|
247
|
+
originChannel: "telegram",
|
|
248
|
+
decidedAction: "reject",
|
|
249
|
+
hasOriginGuardianReply: true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// The resolver's guardian-facing reply is being delivered in that chat;
|
|
253
|
+
// withdrawal only drops the keyboard.
|
|
254
|
+
expect(withdrawTelegramApprovalCard).toHaveBeenCalledWith({
|
|
255
|
+
chatId: "T1",
|
|
256
|
+
messageId: "9",
|
|
257
|
+
status: "denied",
|
|
258
|
+
decidedAction: "reject",
|
|
259
|
+
postStatusReply: false,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("posts the Telegram status reply for origin decisions without a guardian-facing resolver reply", async () => {
|
|
264
|
+
const req = makeRequest({ sourceChannel: "telegram" });
|
|
265
|
+
bridgeState.seedDelivery({
|
|
266
|
+
requestId: req.id,
|
|
267
|
+
destinationChannel: "telegram",
|
|
268
|
+
destinationChatId: "T1",
|
|
269
|
+
destinationMessageId: "9",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Most resolvers (tool grants, tool approvals, questions) reply to the
|
|
273
|
+
// requester only, so the quoted status reply is the guardian's only
|
|
274
|
+
// durable outcome even for a decision made on Telegram itself.
|
|
275
|
+
await withdrawGuardianRequestCards({
|
|
276
|
+
request: req,
|
|
277
|
+
status: "approved",
|
|
278
|
+
originChannel: "telegram",
|
|
279
|
+
decidedAction: "approve_once",
|
|
280
|
+
hasOriginGuardianReply: false,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(withdrawTelegramApprovalCard).toHaveBeenCalledWith({
|
|
284
|
+
chatId: "T1",
|
|
285
|
+
messageId: "9",
|
|
286
|
+
status: "approved",
|
|
287
|
+
decidedAction: "approve_once",
|
|
288
|
+
postStatusReply: true,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("skips the Telegram edit when no channel message id was captured", async () => {
|
|
293
|
+
const req = makeRequest({ sourceChannel: "telegram" });
|
|
294
|
+
bridgeState.seedDelivery({
|
|
295
|
+
requestId: req.id,
|
|
296
|
+
destinationChannel: "telegram",
|
|
297
|
+
destinationChatId: "T1",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await withdrawGuardianRequestCards({ request: req, status: "approved" });
|
|
301
|
+
|
|
302
|
+
expect(withdrawTelegramApprovalCard).not.toHaveBeenCalled();
|
|
215
303
|
});
|
|
216
304
|
|
|
217
305
|
test("is best-effort: a failing surface never blocks the others or throws", async () => {
|
|
@@ -442,6 +442,72 @@ describe("HTTP POST /v1/messages clientTimezone transport metadata", () => {
|
|
|
442
442
|
});
|
|
443
443
|
});
|
|
444
444
|
|
|
445
|
+
describe("HTTP POST /v1/messages visibleAppId transport metadata", () => {
|
|
446
|
+
beforeEach(() => {
|
|
447
|
+
routeGuardianReplyMock.mockClear();
|
|
448
|
+
addMessageMock.mockClear();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
async function captureTransport(
|
|
452
|
+
body: Record<string, unknown>,
|
|
453
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
454
|
+
const conversation = makeConversation({
|
|
455
|
+
persistUserMessage: mock(async () => ({
|
|
456
|
+
id: "persisted-msg-id",
|
|
457
|
+
deduplicated: false,
|
|
458
|
+
})),
|
|
459
|
+
runAgentLoop: mock(async () => undefined),
|
|
460
|
+
});
|
|
461
|
+
let capturedOptions: Record<string, unknown> | undefined;
|
|
462
|
+
|
|
463
|
+
const res = await sendMessage("hello", conversation, body, {
|
|
464
|
+
onGetOrCreateConversation: (_conversationId, opts) => {
|
|
465
|
+
capturedOptions = opts;
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(res.status).toBe(202);
|
|
470
|
+
return capturedOptions?.transport as Record<string, unknown> | undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
test("carries the reported visible app onto the transport", async () => {
|
|
474
|
+
expect(await captureTransport({ visibleAppId: "app-abc" })).toEqual({
|
|
475
|
+
channelId: "vellum",
|
|
476
|
+
interfaceId: "macos",
|
|
477
|
+
visibleAppId: "app-abc",
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("omits visibleAppId when no app is in view", async () => {
|
|
482
|
+
expect(await captureTransport({})).toEqual({
|
|
483
|
+
channelId: "vellum",
|
|
484
|
+
interfaceId: "macos",
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("carries a long plugin-bundled app id, which the viewer can open", async () => {
|
|
489
|
+
// `plugins~<plugin>~<app>`, both segments at the filesystem's 255-byte
|
|
490
|
+
// ceiling. The open route resolves ids this long, so ingress must not clip
|
|
491
|
+
// them or the app on screen goes unreported.
|
|
492
|
+
const longPluginAppId = `plugins~${"p".repeat(255)}~${"a".repeat(255)}`;
|
|
493
|
+
|
|
494
|
+
expect(await captureTransport({ visibleAppId: longPluginAppId })).toEqual({
|
|
495
|
+
channelId: "vellum",
|
|
496
|
+
interfaceId: "macos",
|
|
497
|
+
visibleAppId: longPluginAppId,
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("drops a traversal-shaped visibleAppId without rejecting the message", async () => {
|
|
502
|
+
expect(
|
|
503
|
+
await captureTransport({ visibleAppId: "../../etc/passwd" }),
|
|
504
|
+
).toEqual({
|
|
505
|
+
channelId: "vellum",
|
|
506
|
+
interfaceId: "macos",
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
445
511
|
// ============================================================================
|
|
446
512
|
// CLIENT METADATA — sanitized x-vellum-* headers persisted under
|
|
447
513
|
// metadata.client for turn analytics
|
|
@@ -193,6 +193,93 @@ describe("handleListMessages attachments", () => {
|
|
|
193
193
|
expect(docAtt!.data).toBeUndefined();
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
test("contentBlocks attachment blocks are metadata-only references", async () => {
|
|
197
|
+
// The flat `attachments` array is the sole payload carrier. An inline
|
|
198
|
+
// file-block ref places an `attachment` block in `contentBlocks`, but
|
|
199
|
+
// that block must reference the row by id without repeating its base64:
|
|
200
|
+
// duplicating it doubles every image in the response and in client
|
|
201
|
+
// memory.
|
|
202
|
+
const conv = createConversation();
|
|
203
|
+
const stored = uploadAttachment("photo.png", "image/png", IMAGE_BASE64);
|
|
204
|
+
const msg = await addMessage(
|
|
205
|
+
conv.id,
|
|
206
|
+
"user",
|
|
207
|
+
JSON.stringify([
|
|
208
|
+
{ type: "text", text: "check this image" },
|
|
209
|
+
{
|
|
210
|
+
type: "file",
|
|
211
|
+
source: { type: "workspace_ref", attachmentId: stored.id },
|
|
212
|
+
filename: "photo.png",
|
|
213
|
+
media_type: "image/png",
|
|
214
|
+
},
|
|
215
|
+
]),
|
|
216
|
+
);
|
|
217
|
+
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
218
|
+
|
|
219
|
+
const response = handleListMessages(createTestArgs(conv.id));
|
|
220
|
+
const body = response as {
|
|
221
|
+
messages: {
|
|
222
|
+
attachments?: AttachmentPayload[];
|
|
223
|
+
contentBlocks?: Array<{
|
|
224
|
+
type: string;
|
|
225
|
+
attachment?: {
|
|
226
|
+
id: string;
|
|
227
|
+
data?: string;
|
|
228
|
+
thumbnailData?: string;
|
|
229
|
+
};
|
|
230
|
+
}>;
|
|
231
|
+
}[];
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
expect(body.messages).toHaveLength(1);
|
|
235
|
+
const row = body.messages[0];
|
|
236
|
+
|
|
237
|
+
// Payload ships on the flat array.
|
|
238
|
+
expect(row.attachments).toHaveLength(1);
|
|
239
|
+
expect(row.attachments![0].data).toBe(IMAGE_BASE64);
|
|
240
|
+
|
|
241
|
+
// The inline block references the same row without the payload.
|
|
242
|
+
const blocks = (row.contentBlocks ?? []).filter(
|
|
243
|
+
(b) => b.type === "attachment",
|
|
244
|
+
);
|
|
245
|
+
expect(blocks).toHaveLength(1);
|
|
246
|
+
expect(blocks[0].attachment!.id).toBe(stored.id);
|
|
247
|
+
expect(blocks[0].attachment!.data).toBeUndefined();
|
|
248
|
+
expect(blocks[0].attachment!.thumbnailData).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("synthesized attachment blocks are metadata-only references", async () => {
|
|
252
|
+
// Directive attachments take the appendix path (no file-block ref), which
|
|
253
|
+
// must strip the payload the same way the inline path does.
|
|
254
|
+
const conv = createConversation();
|
|
255
|
+
const msg = await addMessage(
|
|
256
|
+
conv.id,
|
|
257
|
+
"assistant",
|
|
258
|
+
JSON.stringify([{ type: "text", text: "" }]),
|
|
259
|
+
);
|
|
260
|
+
const stored = uploadAttachment("output.png", "image/png", IMAGE_BASE64);
|
|
261
|
+
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
262
|
+
|
|
263
|
+
const response = handleListMessages(createTestArgs(conv.id));
|
|
264
|
+
const body = response as {
|
|
265
|
+
messages: {
|
|
266
|
+
attachments?: AttachmentPayload[];
|
|
267
|
+
contentBlocks?: Array<{
|
|
268
|
+
type: string;
|
|
269
|
+
attachment?: { id: string; data?: string };
|
|
270
|
+
}>;
|
|
271
|
+
}[];
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const row = body.messages[0];
|
|
275
|
+
expect(row.attachments![0].data).toBe(IMAGE_BASE64);
|
|
276
|
+
const blocks = (row.contentBlocks ?? []).filter(
|
|
277
|
+
(b) => b.type === "attachment",
|
|
278
|
+
);
|
|
279
|
+
expect(blocks).toHaveLength(1);
|
|
280
|
+
expect(blocks[0].attachment!.data).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
|
|
196
283
|
test("attachment-only assistant message synthesizes contentBlocks", async () => {
|
|
197
284
|
// When the assistant's entire response was a <vellum-attachment/> tag,
|
|
198
285
|
// parseDirectives strips it → cleanText is empty → renderHistoryContent
|