@vellumai/assistant 0.11.8 → 0.11.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/gemini-provider.test.ts +46 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +19 -6
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +15 -9
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +47 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -104,6 +104,58 @@ describe("handleListMessages system-card projection", () => {
|
|
|
104
104
|
expect(silentRow?.noResponse).toBe(true);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
test("projects a reaction's typed emoji fields to the response", async () => {
|
|
108
|
+
const conv = createConversation();
|
|
109
|
+
const row = await addMessage(
|
|
110
|
+
conv.id,
|
|
111
|
+
"user",
|
|
112
|
+
JSON.stringify([{ type: "text", text: "[reaction]" }]),
|
|
113
|
+
{
|
|
114
|
+
metadata: {
|
|
115
|
+
messageKind: "reaction",
|
|
116
|
+
providerMeta: JSON.stringify({
|
|
117
|
+
source: "discord",
|
|
118
|
+
conversationExternalId: "chan-1",
|
|
119
|
+
eventKind: "reaction",
|
|
120
|
+
reaction: {
|
|
121
|
+
targetMessageId: "555.1",
|
|
122
|
+
emoji: "<:party_blob:111>",
|
|
123
|
+
emojiKind: "custom",
|
|
124
|
+
emojiName: "party_blob",
|
|
125
|
+
emojiId: "111",
|
|
126
|
+
emojiAnimated: true,
|
|
127
|
+
op: "added",
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const response = (await handleListMessages({
|
|
135
|
+
queryParams: { conversationId: conv.id },
|
|
136
|
+
})) as {
|
|
137
|
+
messages: Array<
|
|
138
|
+
ProjectedMessage & {
|
|
139
|
+
reaction?: {
|
|
140
|
+
emoji: string;
|
|
141
|
+
emojiKind?: string;
|
|
142
|
+
emojiName?: string;
|
|
143
|
+
emojiId?: string;
|
|
144
|
+
emojiAnimated?: boolean;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
>;
|
|
148
|
+
};
|
|
149
|
+
const projected = response.messages.find((m) => m.id === row.id);
|
|
150
|
+
expect(projected?.reaction).toMatchObject({
|
|
151
|
+
emoji: "<:party_blob:111>",
|
|
152
|
+
emojiKind: "custom",
|
|
153
|
+
emojiName: "party_blob",
|
|
154
|
+
emojiId: "111",
|
|
155
|
+
emojiAnimated: true,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
107
159
|
test("projects the reaction fact from a reaction row's envelope", async () => {
|
|
108
160
|
const conv = createConversation();
|
|
109
161
|
const row = await addMessage(
|
|
@@ -144,6 +196,57 @@ describe("handleListMessages system-card projection", () => {
|
|
|
144
196
|
expect(projected?.reaction?.selfAuthored).toBe(true);
|
|
145
197
|
});
|
|
146
198
|
|
|
199
|
+
test("projects deletedAt from a row deleted on its channel", async () => {
|
|
200
|
+
// Mirror the delete propagation stamp: the row keeps its content and the
|
|
201
|
+
// envelope carries the marker, in either shape the daemon writes.
|
|
202
|
+
const conv = createConversation();
|
|
203
|
+
const discordRow = await addMessage(
|
|
204
|
+
conv.id,
|
|
205
|
+
"user",
|
|
206
|
+
JSON.stringify([{ type: "text", text: "A message later deleted" }]),
|
|
207
|
+
{
|
|
208
|
+
metadata: {
|
|
209
|
+
providerMeta: JSON.stringify({
|
|
210
|
+
source: "discord",
|
|
211
|
+
conversationExternalId: "chan-1",
|
|
212
|
+
messageId: "111",
|
|
213
|
+
eventKind: "message",
|
|
214
|
+
deletedAt: 1725100000000,
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
const slackRow = await addMessage(
|
|
220
|
+
conv.id,
|
|
221
|
+
"assistant",
|
|
222
|
+
JSON.stringify([{ type: "text", text: "The assistant's deleted post" }]),
|
|
223
|
+
{
|
|
224
|
+
metadata: {
|
|
225
|
+
slackMeta: JSON.stringify({
|
|
226
|
+
source: "slack",
|
|
227
|
+
channelId: "C0123",
|
|
228
|
+
channelTs: "1725100000.000100",
|
|
229
|
+
eventKind: "message",
|
|
230
|
+
deletedAt: 1725100001000,
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const response = (await handleListMessages({
|
|
237
|
+
queryParams: { conversationId: conv.id },
|
|
238
|
+
})) as { messages: Array<ProjectedMessage & { deletedAt?: number }> };
|
|
239
|
+
for (const message of response.messages) {
|
|
240
|
+
expect(() => ConversationMessageSchema.parse(message)).not.toThrow();
|
|
241
|
+
}
|
|
242
|
+
expect(
|
|
243
|
+
response.messages.find((m) => m.id === discordRow.id)?.deletedAt,
|
|
244
|
+
).toBe(1725100000000);
|
|
245
|
+
expect(response.messages.find((m) => m.id === slackRow.id)?.deletedAt).toBe(
|
|
246
|
+
1725100001000,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
147
250
|
test("omits systemCard on ordinary user and assistant rows", async () => {
|
|
148
251
|
const conv = createConversation();
|
|
149
252
|
await addMessage(
|
|
@@ -414,3 +414,80 @@ describe("unsendable-image notice", () => {
|
|
|
414
414
|
);
|
|
415
415
|
});
|
|
416
416
|
});
|
|
417
|
+
|
|
418
|
+
describe("resolveMediaReferences file workspace_ref stay-on-disk", () => {
|
|
419
|
+
beforeEach(resetTables);
|
|
420
|
+
|
|
421
|
+
test("does not inline over-cap text or extracted_text", async () => {
|
|
422
|
+
const conv = createConversation();
|
|
423
|
+
const stored = await createInlineAttachment(
|
|
424
|
+
conv.id,
|
|
425
|
+
conv.createdAt,
|
|
426
|
+
"oversized-content.txt",
|
|
427
|
+
"text/plain",
|
|
428
|
+
Buffer.from("hello-over-cap").toString("base64"),
|
|
429
|
+
);
|
|
430
|
+
const messages: Message[] = [
|
|
431
|
+
userMessage([
|
|
432
|
+
{
|
|
433
|
+
type: "file",
|
|
434
|
+
source: {
|
|
435
|
+
type: "workspace_ref",
|
|
436
|
+
media_type: "text/plain",
|
|
437
|
+
attachmentId: stored.id,
|
|
438
|
+
sizeBytes: 8_000_001,
|
|
439
|
+
filename: "oversized-content.txt",
|
|
440
|
+
},
|
|
441
|
+
extracted_text: "this dump must not be resent",
|
|
442
|
+
},
|
|
443
|
+
]),
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
const resolved = await resolveMediaReferences(messages);
|
|
447
|
+
const block = resolved[0]!.content[0]!;
|
|
448
|
+
expect(block.type).toBe("file");
|
|
449
|
+
if (block.type !== "file") {
|
|
450
|
+
throw new Error("expected a file block");
|
|
451
|
+
}
|
|
452
|
+
expect(block.source.type).toBe("workspace_ref");
|
|
453
|
+
expect(block.extracted_text).toBeUndefined();
|
|
454
|
+
expect(JSON.stringify(resolved)).not.toContain("this dump must not be resent");
|
|
455
|
+
expect(JSON.stringify(resolved)).not.toContain("hello-over-cap");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("does not inline video bytes or extracted_text", async () => {
|
|
459
|
+
const conv = createConversation();
|
|
460
|
+
const stored = await createInlineAttachment(
|
|
461
|
+
conv.id,
|
|
462
|
+
conv.createdAt,
|
|
463
|
+
"clip.mp4",
|
|
464
|
+
"video/mp4",
|
|
465
|
+
Buffer.from("not-a-real-video").toString("base64"),
|
|
466
|
+
);
|
|
467
|
+
const messages: Message[] = [
|
|
468
|
+
userMessage([
|
|
469
|
+
{
|
|
470
|
+
type: "file",
|
|
471
|
+
source: {
|
|
472
|
+
type: "workspace_ref",
|
|
473
|
+
media_type: "video/mp4",
|
|
474
|
+
attachmentId: stored.id,
|
|
475
|
+
sizeBytes: stored.sizeBytes,
|
|
476
|
+
filename: "clip.mp4",
|
|
477
|
+
},
|
|
478
|
+
extracted_text: "dump of the video as text",
|
|
479
|
+
},
|
|
480
|
+
]),
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
const resolved = await resolveMediaReferences(messages);
|
|
484
|
+
const block = resolved[0]!.content[0]!;
|
|
485
|
+
expect(block.type).toBe("file");
|
|
486
|
+
if (block.type !== "file") {
|
|
487
|
+
throw new Error("expected a file block");
|
|
488
|
+
}
|
|
489
|
+
expect(block.source.type).toBe("workspace_ref");
|
|
490
|
+
expect(block.extracted_text).toBeUndefined();
|
|
491
|
+
expect(JSON.stringify(resolved)).not.toContain("dump of the video");
|
|
492
|
+
});
|
|
493
|
+
});
|
|
@@ -88,7 +88,7 @@ describe("notification decision fallback copy", () => {
|
|
|
88
88
|
);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
test("enforces free-text answer instructions
|
|
91
|
+
test("enforces free-text answer instructions into chat copy and keeps them out of vellum copy", async () => {
|
|
92
92
|
const signal = makeSignal({
|
|
93
93
|
contextPayload: {
|
|
94
94
|
requestId: "req-pending-1",
|
|
@@ -101,13 +101,20 @@ describe("notification decision fallback copy", () => {
|
|
|
101
101
|
});
|
|
102
102
|
const decision = await evaluateSignal(signal, [
|
|
103
103
|
"vellum",
|
|
104
|
+
"telegram",
|
|
104
105
|
] as NotificationChannel[]);
|
|
105
106
|
|
|
106
107
|
expect(decision.fallbackUsed).toBe(true);
|
|
107
|
-
expect(decision.renderedCopy.
|
|
108
|
-
expect(decision.renderedCopy.
|
|
109
|
-
expect(decision.renderedCopy.
|
|
110
|
-
expect(decision.renderedCopy.
|
|
108
|
+
expect(decision.renderedCopy.telegram?.body).toContain("A1B2C3");
|
|
109
|
+
expect(decision.renderedCopy.telegram?.body).toContain("<your answer>");
|
|
110
|
+
expect(decision.renderedCopy.telegram?.body).not.toContain("approve");
|
|
111
|
+
expect(decision.renderedCopy.telegram?.body).not.toContain("reject");
|
|
112
|
+
// The bell and the banner read the vellum copy and act through the
|
|
113
|
+
// card, so the question arrives without the chat channel's mechanics.
|
|
114
|
+
expect(decision.renderedCopy.vellum?.body).toBe("What is the gate code?");
|
|
115
|
+
expect(decision.renderedCopy.vellum?.conversationSeedMessage).toBe(
|
|
116
|
+
"What is the gate code?",
|
|
117
|
+
);
|
|
111
118
|
});
|
|
112
119
|
|
|
113
120
|
test("enforcement appends free-text answer instructions when LLM copy only mentions request code", async () => {
|
|
@@ -118,10 +125,10 @@ describe("notification decision fallback copy", () => {
|
|
|
118
125
|
name: "record_notification_decision",
|
|
119
126
|
input: {
|
|
120
127
|
shouldNotify: true,
|
|
121
|
-
selectedChannels: ["
|
|
128
|
+
selectedChannels: ["telegram"],
|
|
122
129
|
reasoningSummary: "LLM decision",
|
|
123
130
|
renderedCopy: {
|
|
124
|
-
|
|
131
|
+
telegram: {
|
|
125
132
|
title: "Guardian Question",
|
|
126
133
|
body: "Use reference code A1B2C3 for this request.",
|
|
127
134
|
},
|
|
@@ -143,17 +150,19 @@ describe("notification decision fallback copy", () => {
|
|
|
143
150
|
});
|
|
144
151
|
|
|
145
152
|
const decision = await evaluateSignal(signal, [
|
|
146
|
-
"
|
|
153
|
+
"telegram",
|
|
147
154
|
] as NotificationChannel[]);
|
|
148
155
|
|
|
149
156
|
expect(decision.fallbackUsed).toBe(false);
|
|
150
|
-
expect(decision.renderedCopy.
|
|
157
|
+
expect(decision.renderedCopy.telegram?.body).toContain(
|
|
151
158
|
'"A1B2C3 <your answer>"',
|
|
152
159
|
);
|
|
153
|
-
expect(decision.renderedCopy.
|
|
160
|
+
expect(decision.renderedCopy.telegram?.body).not.toContain(
|
|
154
161
|
'"A1B2C3 approve"',
|
|
155
162
|
);
|
|
156
|
-
expect(decision.renderedCopy.
|
|
163
|
+
expect(decision.renderedCopy.telegram?.body).not.toContain(
|
|
164
|
+
'"A1B2C3 reject"',
|
|
165
|
+
);
|
|
157
166
|
});
|
|
158
167
|
|
|
159
168
|
test("enforcement appends answer instructions when LLM copy incorrectly uses approve/reject wording", async () => {
|
|
@@ -164,10 +173,10 @@ describe("notification decision fallback copy", () => {
|
|
|
164
173
|
name: "record_notification_decision",
|
|
165
174
|
input: {
|
|
166
175
|
shouldNotify: true,
|
|
167
|
-
selectedChannels: ["
|
|
176
|
+
selectedChannels: ["telegram"],
|
|
168
177
|
reasoningSummary: "LLM decision",
|
|
169
178
|
renderedCopy: {
|
|
170
|
-
|
|
179
|
+
telegram: {
|
|
171
180
|
title: "Guardian Question",
|
|
172
181
|
body: 'Reference code: A1B2C3. Reply "A1B2C3 approve" or "A1B2C3 reject".',
|
|
173
182
|
},
|
|
@@ -189,11 +198,11 @@ describe("notification decision fallback copy", () => {
|
|
|
189
198
|
});
|
|
190
199
|
|
|
191
200
|
const decision = await evaluateSignal(signal, [
|
|
192
|
-
"
|
|
201
|
+
"telegram",
|
|
193
202
|
] as NotificationChannel[]);
|
|
194
203
|
|
|
195
204
|
expect(decision.fallbackUsed).toBe(false);
|
|
196
|
-
expect(decision.renderedCopy.
|
|
205
|
+
expect(decision.renderedCopy.telegram?.body).toContain(
|
|
197
206
|
'"A1B2C3 <your answer>"',
|
|
198
207
|
);
|
|
199
208
|
});
|
|
@@ -206,10 +215,10 @@ describe("notification decision fallback copy", () => {
|
|
|
206
215
|
name: "record_notification_decision",
|
|
207
216
|
input: {
|
|
208
217
|
shouldNotify: true,
|
|
209
|
-
selectedChannels: ["
|
|
218
|
+
selectedChannels: ["telegram"],
|
|
210
219
|
reasoningSummary: "LLM decision",
|
|
211
220
|
renderedCopy: {
|
|
212
|
-
|
|
221
|
+
telegram: {
|
|
213
222
|
title: "Guardian Question",
|
|
214
223
|
body: "Use reference code A1B2C3 for this request.",
|
|
215
224
|
},
|
|
@@ -230,12 +239,12 @@ describe("notification decision fallback copy", () => {
|
|
|
230
239
|
});
|
|
231
240
|
|
|
232
241
|
const decision = await evaluateSignal(signal, [
|
|
233
|
-
"
|
|
242
|
+
"telegram",
|
|
234
243
|
] as NotificationChannel[]);
|
|
235
244
|
|
|
236
245
|
expect(decision.fallbackUsed).toBe(false);
|
|
237
|
-
expect(decision.renderedCopy.
|
|
238
|
-
expect(decision.renderedCopy.
|
|
246
|
+
expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 approve"');
|
|
247
|
+
expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 reject"');
|
|
239
248
|
});
|
|
240
249
|
|
|
241
250
|
test("approval-mode enforcement removes conflicting answer-mode phrasing", async () => {
|
|
@@ -246,10 +255,10 @@ describe("notification decision fallback copy", () => {
|
|
|
246
255
|
name: "record_notification_decision",
|
|
247
256
|
input: {
|
|
248
257
|
shouldNotify: true,
|
|
249
|
-
selectedChannels: ["
|
|
258
|
+
selectedChannels: ["telegram"],
|
|
250
259
|
reasoningSummary: "LLM decision",
|
|
251
260
|
renderedCopy: {
|
|
252
|
-
|
|
261
|
+
telegram: {
|
|
253
262
|
title: "Guardian Question",
|
|
254
263
|
body: 'Reference code: A1B2C3. Reply "A1B2C3 <your answer>".',
|
|
255
264
|
},
|
|
@@ -270,13 +279,13 @@ describe("notification decision fallback copy", () => {
|
|
|
270
279
|
});
|
|
271
280
|
|
|
272
281
|
const decision = await evaluateSignal(signal, [
|
|
273
|
-
"
|
|
282
|
+
"telegram",
|
|
274
283
|
] as NotificationChannel[]);
|
|
275
284
|
|
|
276
285
|
expect(decision.fallbackUsed).toBe(false);
|
|
277
|
-
expect(decision.renderedCopy.
|
|
278
|
-
expect(decision.renderedCopy.
|
|
279
|
-
expect(decision.renderedCopy.
|
|
286
|
+
expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 approve"');
|
|
287
|
+
expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 reject"');
|
|
288
|
+
expect(decision.renderedCopy.telegram?.body).not.toContain("<your answer>");
|
|
280
289
|
});
|
|
281
290
|
|
|
282
291
|
test("slack approval copy is stripped of request-code instructions instead of enforced", async () => {
|
|
@@ -292,12 +301,18 @@ describe("notification decision fallback copy", () => {
|
|
|
292
301
|
|
|
293
302
|
const decision = await evaluateSignal(signal, [
|
|
294
303
|
"vellum",
|
|
304
|
+
"telegram",
|
|
295
305
|
"slack",
|
|
296
306
|
] as NotificationChannel[]);
|
|
297
307
|
|
|
298
308
|
expect(decision.fallbackUsed).toBe(true);
|
|
299
|
-
//
|
|
300
|
-
expect(decision.renderedCopy.
|
|
309
|
+
// Telegram answers by typed reply and keeps the code-reply directive.
|
|
310
|
+
expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 approve"');
|
|
311
|
+
// Vellum acts through the card: the same text as Slack, code-free.
|
|
312
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
313
|
+
decision.renderedCopy.slack?.body,
|
|
314
|
+
);
|
|
315
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain("A1B2C3");
|
|
301
316
|
// Slack renders Approve/Reject buttons — no code anywhere in its copy.
|
|
302
317
|
expect(decision.renderedCopy.slack?.body).toBe(
|
|
303
318
|
"Approve tool: bash — ls /tmp (requested by Alice)",
|
|
@@ -374,6 +389,110 @@ describe("notification decision fallback copy", () => {
|
|
|
374
389
|
});
|
|
375
390
|
});
|
|
376
391
|
|
|
392
|
+
// ── Surfaces that act through the card ───────────────────────────────────
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The vellum copy is read by the bell detail, the OS banner, and the
|
|
396
|
+
* delivery audit row, and the platform push mirrors it. None of them takes
|
|
397
|
+
* a typed reply, so the chat channels' "Reference code / Reply CODE" copy
|
|
398
|
+
* is noise there and is kept out on every decision path.
|
|
399
|
+
*/
|
|
400
|
+
describe("guardian copy for surfaces that act through the card", () => {
|
|
401
|
+
beforeEach(() => {
|
|
402
|
+
configuredProvider = null;
|
|
403
|
+
extractedToolUse = null;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("a question with options reaches the bell as the question and its options, nothing else", async () => {
|
|
407
|
+
const signal = makeSignal({
|
|
408
|
+
sourceChannel: "telegram",
|
|
409
|
+
contextPayload: {
|
|
410
|
+
requestId: "req-ask-bell-1",
|
|
411
|
+
requestCode: "E96831",
|
|
412
|
+
requestKind: "pending_question",
|
|
413
|
+
questionText:
|
|
414
|
+
"When you say it feels too harsh, what is the feeling exactly?",
|
|
415
|
+
options: [
|
|
416
|
+
{ id: "heavy", label: "Lines too heavy" },
|
|
417
|
+
{ id: "showy", label: "Feels showy" },
|
|
418
|
+
{ id: "both", label: "Both, honestly" },
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
const decision = await evaluateSignal(signal, [
|
|
423
|
+
"vellum",
|
|
424
|
+
"telegram",
|
|
425
|
+
] as NotificationChannel[]);
|
|
426
|
+
|
|
427
|
+
const expectedQuestion =
|
|
428
|
+
"When you say it feels too harsh, what is the feeling exactly?\n\n1. Lines too heavy\n2. Feels showy\n3. Both, honestly";
|
|
429
|
+
// The bell reads the seed; the banner reads the body. Line structure is
|
|
430
|
+
// preserved so the options render as lines rather than one run-on blob.
|
|
431
|
+
expect(decision.renderedCopy.vellum?.conversationSeedMessage).toBe(
|
|
432
|
+
expectedQuestion,
|
|
433
|
+
);
|
|
434
|
+
expect(decision.renderedCopy.vellum?.body).toBe(expectedQuestion);
|
|
435
|
+
expect(decision.renderedCopy.vellum?.deliveryText).toBe(expectedQuestion);
|
|
436
|
+
// The chat channel still carries the answer mechanics after the options.
|
|
437
|
+
expect(decision.renderedCopy.telegram?.deliveryText).toBe(
|
|
438
|
+
`${expectedQuestion}\n\nReference code: E96831. Reply "E96831 <your answer>".`,
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("model-authored code phrasing is stripped from vellum and platform copy", async () => {
|
|
443
|
+
configuredProvider = {
|
|
444
|
+
sendMessage: async () => ({ content: [] }),
|
|
445
|
+
};
|
|
446
|
+
extractedToolUse = {
|
|
447
|
+
name: "record_notification_decision",
|
|
448
|
+
input: {
|
|
449
|
+
shouldNotify: true,
|
|
450
|
+
selectedChannels: ["vellum", "platform"],
|
|
451
|
+
reasoningSummary: "LLM decision",
|
|
452
|
+
renderedCopy: {
|
|
453
|
+
vellum: {
|
|
454
|
+
title: "Tool Grant Request",
|
|
455
|
+
body: 'Alice is asking to run ls /tmp.\nApproval code: A1B2C3\nReply "A1B2C3 approve" or "A1B2C3 reject".',
|
|
456
|
+
conversationSeedMessage:
|
|
457
|
+
"**Alice** is asking to run `ls /tmp`.\n\nReference code: A1B2C3.",
|
|
458
|
+
},
|
|
459
|
+
platform: {
|
|
460
|
+
title: "Tool Grant Request",
|
|
461
|
+
body: 'Alice is asking to run ls /tmp. Reply "A1B2C3 approve" or "A1B2C3 reject".',
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
dedupeKey: "guardian-question-vellum-llm-test",
|
|
465
|
+
confidence: 0.9,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const signal = makeSignal({
|
|
470
|
+
contextPayload: {
|
|
471
|
+
requestId: "req-grant-vellum-1",
|
|
472
|
+
questionText: "Approve tool: bash (requested by Alice)",
|
|
473
|
+
requestCode: "A1B2C3",
|
|
474
|
+
requestKind: "tool_grant_request",
|
|
475
|
+
toolName: "bash",
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
const decision = await evaluateSignal(signal, [
|
|
479
|
+
"vellum",
|
|
480
|
+
"platform",
|
|
481
|
+
] as NotificationChannel[]);
|
|
482
|
+
|
|
483
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
484
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
485
|
+
"Alice is asking to run ls /tmp.",
|
|
486
|
+
);
|
|
487
|
+
expect(decision.renderedCopy.vellum?.conversationSeedMessage).toBe(
|
|
488
|
+
"**Alice** is asking to run `ls /tmp`.",
|
|
489
|
+
);
|
|
490
|
+
expect(decision.renderedCopy.platform?.body).toBe(
|
|
491
|
+
"Alice is asking to run ls /tmp.",
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
377
496
|
// ── Access-request instruction enforcement ──────────────────────────────
|
|
378
497
|
|
|
379
498
|
describe("access-request instruction enforcement", () => {
|
|
@@ -406,22 +525,24 @@ describe("access-request instruction enforcement", () => {
|
|
|
406
525
|
};
|
|
407
526
|
}
|
|
408
527
|
|
|
409
|
-
test("fallback copy
|
|
528
|
+
test("fallback copy carries the requester context and no reply mechanics", async () => {
|
|
410
529
|
const signal = makeAccessRequestSignal();
|
|
411
530
|
const decision = await evaluateSignal(signal, [
|
|
412
531
|
"vellum",
|
|
532
|
+
"telegram",
|
|
413
533
|
] as NotificationChannel[]);
|
|
414
534
|
|
|
415
535
|
expect(decision.fallbackUsed).toBe(true);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
536
|
+
for (const channel of ["vellum", "telegram"] as const) {
|
|
537
|
+
const body = decision.renderedCopy[channel]?.body ?? "";
|
|
538
|
+
expect(body).toContain("Alice");
|
|
539
|
+
expect(body).not.toContain("A1B2C3");
|
|
540
|
+
// Context, not mechanics: no surface offers an invite button.
|
|
541
|
+
expect(body).toContain('Reply "open invite flow"');
|
|
542
|
+
}
|
|
422
543
|
});
|
|
423
544
|
|
|
424
|
-
test("
|
|
545
|
+
test("model copy with no mechanics keeps its text and gains the invite sentence", async () => {
|
|
425
546
|
configuredProvider = {
|
|
426
547
|
sendMessage: async () => ({ content: [] }),
|
|
427
548
|
};
|
|
@@ -448,13 +569,12 @@ describe("access-request instruction enforcement", () => {
|
|
|
448
569
|
] as NotificationChannel[]);
|
|
449
570
|
|
|
450
571
|
expect(decision.fallbackUsed).toBe(false);
|
|
451
|
-
expect(decision.renderedCopy.vellum?.body).
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
expect(decision.renderedCopy.vellum?.body).toContain("open invite flow");
|
|
572
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
573
|
+
'Someone wants access to your assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
|
|
574
|
+
);
|
|
455
575
|
});
|
|
456
576
|
|
|
457
|
-
test("
|
|
577
|
+
test("model-authored code directives are stripped, leaving the ask", async () => {
|
|
458
578
|
configuredProvider = {
|
|
459
579
|
sendMessage: async () => ({ content: [] }),
|
|
460
580
|
};
|
|
@@ -481,10 +601,12 @@ describe("access-request instruction enforcement", () => {
|
|
|
481
601
|
] as NotificationChannel[]);
|
|
482
602
|
|
|
483
603
|
expect(decision.fallbackUsed).toBe(false);
|
|
484
|
-
expect(decision.renderedCopy.vellum?.body).
|
|
604
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
605
|
+
'Alice wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.',
|
|
606
|
+
);
|
|
485
607
|
});
|
|
486
608
|
|
|
487
|
-
test("
|
|
609
|
+
test("the code directive is stripped from model copy and the invite directive stays", async () => {
|
|
488
610
|
const fullBody =
|
|
489
611
|
'Alice wants access.\nReply "A1B2C3 verify" to send them a verification code, "A1B2C3 trust" to trust them without one, "A1B2C3 reject" to leave them unverified, or "A1B2C3 block" to block them.\nReply "open invite flow" to start Trusted Contacts invite flow.';
|
|
490
612
|
configuredProvider = {
|
|
@@ -513,11 +635,14 @@ describe("access-request instruction enforcement", () => {
|
|
|
513
635
|
] as NotificationChannel[]);
|
|
514
636
|
|
|
515
637
|
expect(decision.fallbackUsed).toBe(false);
|
|
516
|
-
|
|
517
|
-
|
|
638
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
639
|
+
'Alice wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.',
|
|
640
|
+
);
|
|
518
641
|
});
|
|
519
642
|
|
|
520
|
-
test("
|
|
643
|
+
test("the strip covers deliveryText and conversationSeedMessage too", async () => {
|
|
644
|
+
const withMechanics =
|
|
645
|
+
'Someone wants access.\nReply "A1B2C3 trust" to trust them, "A1B2C3 reject" to leave them unverified, or "A1B2C3 block" to block them.';
|
|
521
646
|
configuredProvider = {
|
|
522
647
|
sendMessage: async () => ({ content: [] }),
|
|
523
648
|
};
|
|
@@ -530,9 +655,9 @@ describe("access-request instruction enforcement", () => {
|
|
|
530
655
|
renderedCopy: {
|
|
531
656
|
telegram: {
|
|
532
657
|
title: "Access Request",
|
|
533
|
-
body:
|
|
534
|
-
deliveryText:
|
|
535
|
-
conversationSeedMessage:
|
|
658
|
+
body: withMechanics,
|
|
659
|
+
deliveryText: withMechanics,
|
|
660
|
+
conversationSeedMessage: withMechanics,
|
|
536
661
|
},
|
|
537
662
|
},
|
|
538
663
|
dedupeKey: "access-req-multi-field",
|
|
@@ -545,19 +670,16 @@ describe("access-request instruction enforcement", () => {
|
|
|
545
670
|
"telegram",
|
|
546
671
|
] as NotificationChannel[]);
|
|
547
672
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
);
|
|
552
|
-
expect(decision.renderedCopy.telegram?.conversationSeedMessage).
|
|
553
|
-
|
|
554
|
-
);
|
|
555
|
-
expect(decision.renderedCopy.telegram?.conversationSeedMessage).toContain(
|
|
556
|
-
"open invite flow",
|
|
673
|
+
const expected =
|
|
674
|
+
'Someone wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.';
|
|
675
|
+
expect(decision.renderedCopy.telegram?.body).toBe(expected);
|
|
676
|
+
expect(decision.renderedCopy.telegram?.deliveryText).toBe(expected);
|
|
677
|
+
expect(decision.renderedCopy.telegram?.conversationSeedMessage).toBe(
|
|
678
|
+
expected,
|
|
557
679
|
);
|
|
558
680
|
});
|
|
559
681
|
|
|
560
|
-
test("
|
|
682
|
+
test("model copy that leaves the invite sentence out gets it appended", async () => {
|
|
561
683
|
configuredProvider = {
|
|
562
684
|
sendMessage: async () => ({ content: [] }),
|
|
563
685
|
};
|
|
@@ -565,31 +687,33 @@ describe("access-request instruction enforcement", () => {
|
|
|
565
687
|
name: "record_notification_decision",
|
|
566
688
|
input: {
|
|
567
689
|
shouldNotify: true,
|
|
568
|
-
selectedChannels: ["
|
|
690
|
+
selectedChannels: ["telegram"],
|
|
569
691
|
reasoningSummary: "LLM decision",
|
|
570
692
|
renderedCopy: {
|
|
571
|
-
|
|
693
|
+
telegram: {
|
|
572
694
|
title: "Access Request",
|
|
573
|
-
body:
|
|
695
|
+
body: "Alice wants access.",
|
|
696
|
+
deliveryText: "Alice wants access.",
|
|
574
697
|
},
|
|
575
698
|
},
|
|
576
|
-
dedupeKey: "access-req-
|
|
699
|
+
dedupeKey: "access-req-invite-ensured",
|
|
577
700
|
confidence: 0.9,
|
|
578
701
|
},
|
|
579
702
|
};
|
|
580
703
|
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
"vellum",
|
|
704
|
+
const decision = await evaluateSignal(makeAccessRequestSignal(), [
|
|
705
|
+
"telegram",
|
|
584
706
|
] as NotificationChannel[]);
|
|
585
707
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
expect(decision.renderedCopy.
|
|
708
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
709
|
+
const expected =
|
|
710
|
+
'Alice wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.';
|
|
711
|
+
expect(decision.renderedCopy.telegram?.body).toBe(expected);
|
|
712
|
+
expect(decision.renderedCopy.telegram?.deliveryText).toBe(expected);
|
|
713
|
+
expect(decision.renderedCopy.telegram?.title).toBe("Access Request");
|
|
590
714
|
});
|
|
591
715
|
|
|
592
|
-
test("
|
|
716
|
+
test("the invite directive stays even when the request has no code", async () => {
|
|
593
717
|
configuredProvider = {
|
|
594
718
|
sendMessage: async () => ({ content: [] }),
|
|
595
719
|
};
|
|
@@ -602,7 +726,7 @@ describe("access-request instruction enforcement", () => {
|
|
|
602
726
|
renderedCopy: {
|
|
603
727
|
vellum: {
|
|
604
728
|
title: "Access Request",
|
|
605
|
-
body:
|
|
729
|
+
body: 'Someone wants access to your assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
|
|
606
730
|
},
|
|
607
731
|
},
|
|
608
732
|
dedupeKey: "access-req-no-code-invite",
|
|
@@ -622,11 +746,9 @@ describe("access-request instruction enforcement", () => {
|
|
|
622
746
|
] as NotificationChannel[]);
|
|
623
747
|
|
|
624
748
|
expect(decision.fallbackUsed).toBe(false);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
expect(decision.renderedCopy.vellum?.body).not.toContain("approve");
|
|
629
|
-
expect(decision.renderedCopy.vellum?.body).not.toContain("reject");
|
|
749
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
750
|
+
'Someone wants access to your assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
|
|
751
|
+
);
|
|
630
752
|
});
|
|
631
753
|
});
|
|
632
754
|
|