@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
|
@@ -323,6 +323,42 @@ describe("TelegramAdapter", () => {
|
|
|
323
323
|
expect(call.text).toContain("XYZW");
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
+
test("a question with no options is sent as text with its typed-reply instruction", async () => {
|
|
327
|
+
const adapter = new TelegramAdapter();
|
|
328
|
+
const payload = makePayload({
|
|
329
|
+
sourceEventName: "guardian.question",
|
|
330
|
+
copy: {
|
|
331
|
+
title: "Question",
|
|
332
|
+
body: "What time works?",
|
|
333
|
+
deliveryText: "What time works?",
|
|
334
|
+
},
|
|
335
|
+
contextPayload: {
|
|
336
|
+
requestId: "req-voice-1",
|
|
337
|
+
requestCode: "DEF456",
|
|
338
|
+
requestKind: "pending_question",
|
|
339
|
+
questionText: "What time works?",
|
|
340
|
+
},
|
|
341
|
+
approvalContext: {
|
|
342
|
+
requestId: "req-voice-1",
|
|
343
|
+
actions: [],
|
|
344
|
+
plainTextFallback:
|
|
345
|
+
'Reference code: DEF456. Reply "DEF456 <your answer>".',
|
|
346
|
+
intent: "question",
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const result = await adapter.send(payload, makeDestination());
|
|
351
|
+
|
|
352
|
+
expect(result.success).toBe(true);
|
|
353
|
+
expect(sendCalls).toHaveLength(1);
|
|
354
|
+
// No buttons to draw, so no keyboard is attempted and the instruction
|
|
355
|
+
// joins the text: this is the only place the guardian learns the code.
|
|
356
|
+
expect(sendCalls[0]?.approval).toBeUndefined();
|
|
357
|
+
expect(sendCalls[0]?.text).toBe(
|
|
358
|
+
'What time works?\n\nReference code: DEF456. Reply "DEF456 <your answer>".',
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
326
362
|
describe("update", () => {
|
|
327
363
|
test("edits the delivered message in place and keeps its id", async () => {
|
|
328
364
|
const adapter = new TelegramAdapter();
|
|
@@ -1962,7 +1962,7 @@ describe("FireworksProvider reasoning_effort ceiling", () => {
|
|
|
1962
1962
|
test('DeepSeek V4 Pro accepts "max" unclamped', async () => {
|
|
1963
1963
|
const fw = new FireworksProvider(
|
|
1964
1964
|
"fw-key",
|
|
1965
|
-
"accounts/fireworks/models/deepseek-v4-pro",
|
|
1965
|
+
"accounts/fireworks/models/deepseek-v4-pro-0813",
|
|
1966
1966
|
);
|
|
1967
1967
|
await fw.sendMessage([userMsg("hi")], {
|
|
1968
1968
|
systemPrompt: "system",
|
|
@@ -1974,7 +1974,7 @@ describe("FireworksProvider reasoning_effort ceiling", () => {
|
|
|
1974
1974
|
test('DeepSeek V4 Pro accepts "xhigh" unclamped', async () => {
|
|
1975
1975
|
const fw = new FireworksProvider(
|
|
1976
1976
|
"fw-key",
|
|
1977
|
-
"accounts/fireworks/models/deepseek-v4-pro",
|
|
1977
|
+
"accounts/fireworks/models/deepseek-v4-pro-0813",
|
|
1978
1978
|
);
|
|
1979
1979
|
await fw.sendMessage([userMsg("hi")], {
|
|
1980
1980
|
systemPrompt: "system",
|
|
@@ -2015,7 +2015,7 @@ describe("FireworksProvider reasoning_effort ceiling", () => {
|
|
|
2015
2015
|
test("effort below ceiling is forwarded verbatim", async () => {
|
|
2016
2016
|
const fw = new FireworksProvider(
|
|
2017
2017
|
"fw-key",
|
|
2018
|
-
"accounts/fireworks/models/deepseek-v4-pro",
|
|
2018
|
+
"accounts/fireworks/models/deepseek-v4-pro-0813",
|
|
2019
2019
|
);
|
|
2020
2020
|
await fw.sendMessage([userMsg("hi")], {
|
|
2021
2021
|
systemPrompt: "system",
|
|
@@ -2045,7 +2045,7 @@ describe("FireworksProvider reasoning_effort ceiling", () => {
|
|
|
2045
2045
|
systemPrompt: "system",
|
|
2046
2046
|
config: {
|
|
2047
2047
|
effort: "max",
|
|
2048
|
-
model: "accounts/fireworks/models/deepseek-v4-pro",
|
|
2048
|
+
model: "accounts/fireworks/models/deepseek-v4-pro-0813",
|
|
2049
2049
|
},
|
|
2050
2050
|
});
|
|
2051
2051
|
expect(lastCreateParams!.reasoning_effort).toBe("max");
|
|
@@ -2110,7 +2110,7 @@ describe("FireworksProvider sparse reasoning_effort support (GLM 5.3)", () => {
|
|
|
2110
2110
|
|
|
2111
2111
|
test("models without supportedEfforts are unaffected", async () => {
|
|
2112
2112
|
expect(
|
|
2113
|
-
await send("accounts/fireworks/models/deepseek-v4-pro", "medium"),
|
|
2113
|
+
await send("accounts/fireworks/models/deepseek-v4-pro-0813", "medium"),
|
|
2114
2114
|
).toBe("medium");
|
|
2115
2115
|
});
|
|
2116
2116
|
});
|
|
@@ -138,6 +138,13 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
138
138
|
),
|
|
139
139
|
}));
|
|
140
140
|
|
|
141
|
+
const recordedOutboundPosts: Array<Record<string, string>> = [];
|
|
142
|
+
mock.module("../persistence/delivery-crud.js", () => ({
|
|
143
|
+
recordOutboundPost: (post: Record<string, string>) => {
|
|
144
|
+
recordedOutboundPosts.push(post);
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
|
|
141
148
|
mock.module("../persistence/llm-request-log-store.js", () => ({
|
|
142
149
|
recordRequestLog: () => {},
|
|
143
150
|
backfillMessageIdOnLogs: () => {},
|
|
@@ -176,7 +183,7 @@ import {
|
|
|
176
183
|
handleLlmCallStarted,
|
|
177
184
|
handleMessageComplete,
|
|
178
185
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
179
|
-
import {
|
|
186
|
+
import { readSlackMetadataFromMessageMetadata } from "../messaging/providers/slack/message-metadata.js";
|
|
180
187
|
import { deliverReplyViaCallback } from "../runtime/channel-reply-delivery.js";
|
|
181
188
|
import { setConfig } from "./helpers/set-config.js";
|
|
182
189
|
|
|
@@ -266,6 +273,7 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
266
273
|
persistedRows.length = 0;
|
|
267
274
|
setConfig("ui", {});
|
|
268
275
|
nextDeliveryTs = null;
|
|
276
|
+
recordedOutboundPosts.length = 0;
|
|
269
277
|
state = createEventHandlerState();
|
|
270
278
|
state.turnStartedAt = 1_700_000_000_000;
|
|
271
279
|
});
|
|
@@ -276,13 +284,14 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
276
284
|
nextDeliveryTs = null;
|
|
277
285
|
});
|
|
278
286
|
|
|
279
|
-
test("stamps
|
|
287
|
+
test("stamps the neutral envelope with the turn's inbound thread id", async () => {
|
|
280
288
|
const conversationId = "conv-slack-threaded";
|
|
281
289
|
const channelId = "C123CHANNEL";
|
|
282
290
|
|
|
283
|
-
// The turn arrived in a thread
|
|
291
|
+
// The turn arrived in a thread: its inbound thread ts is captured on the
|
|
284
292
|
// trust context (`sourceThreadId`) at ingress, and the reply is stamped
|
|
285
|
-
// from that turn-local value.
|
|
293
|
+
// from that turn-local value. A Slack reply row writes the same neutral
|
|
294
|
+
// envelope every channel writes; Slack's own fields ride its passthrough.
|
|
286
295
|
const deps = makeDeps(conversationId, {
|
|
287
296
|
assistantMessageChannel: "slack",
|
|
288
297
|
requesterChatId: channelId,
|
|
@@ -292,27 +301,29 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
292
301
|
await handleMessageComplete(state, deps, makeMessageCompleteEvent("hi"));
|
|
293
302
|
|
|
294
303
|
const persisted = lastAssistantPersisted();
|
|
295
|
-
|
|
296
|
-
|
|
304
|
+
expect(persisted.metadata?.slackMeta).toBeUndefined();
|
|
305
|
+
const providerMetaRaw = persisted.metadata?.providerMeta;
|
|
306
|
+
expect(typeof providerMetaRaw).toBe("string");
|
|
297
307
|
|
|
298
|
-
const
|
|
308
|
+
const providerMeta = JSON.parse(providerMetaRaw as string) as Record<
|
|
299
309
|
string,
|
|
300
310
|
unknown
|
|
301
311
|
>;
|
|
302
|
-
expect(
|
|
303
|
-
expect(
|
|
304
|
-
expect(
|
|
305
|
-
expect(
|
|
312
|
+
expect(providerMeta.source).toBe("slack");
|
|
313
|
+
expect(providerMeta.eventKind).toBe("message");
|
|
314
|
+
expect(providerMeta.conversationExternalId).toBe(channelId);
|
|
315
|
+
expect(providerMeta.threadId).toBe("1234.5678");
|
|
306
316
|
|
|
307
317
|
// Persistence runs BEFORE the Slack adapter posts the message, so the
|
|
308
|
-
// authoritative
|
|
309
|
-
// The post-send reconciliation in `deliverReplyViaCallback` fills
|
|
310
|
-
//
|
|
311
|
-
// `channel-reply-delivery.test.ts`). Until
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
expect(
|
|
315
|
-
|
|
318
|
+
// authoritative ts (the envelope's `messageId`) is not yet known at this
|
|
319
|
+
// layer. The post-send reconciliation in `deliverReplyViaCallback` fills
|
|
320
|
+
// it once the gateway returns the Slack-assigned ts (covered by
|
|
321
|
+
// `channel-reply-delivery.test.ts`). Until then the row has no Slack
|
|
322
|
+
// view, so a Slack reader gets a clear null rather than a half row.
|
|
323
|
+
expect(providerMeta.messageId).toBeUndefined();
|
|
324
|
+
expect(
|
|
325
|
+
readSlackMetadataFromMessageMetadata(JSON.stringify(persisted.metadata)),
|
|
326
|
+
).toBeNull();
|
|
316
327
|
});
|
|
317
328
|
|
|
318
329
|
test("stamps assistant Slack rows with effective timestamp timezone and no speaker suffix", async () => {
|
|
@@ -335,16 +346,12 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
335
346
|
);
|
|
336
347
|
|
|
337
348
|
const persisted = lastAssistantPersisted();
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
>;
|
|
345
|
-
expect(slackMeta.timestampTimezone).toBe("America/Denver");
|
|
346
|
-
expect(slackMeta.timestampTimezoneLabel).toBe("MT");
|
|
347
|
-
expect(slackMeta.speakerTimezoneLabel).toBeUndefined();
|
|
349
|
+
const providerMeta = JSON.parse(
|
|
350
|
+
persisted.metadata?.providerMeta as string,
|
|
351
|
+
) as Record<string, unknown>;
|
|
352
|
+
expect(providerMeta.timestampTimezone).toBe("America/Denver");
|
|
353
|
+
expect(providerMeta.timestampTimezoneLabel).toBe("MT");
|
|
354
|
+
expect(providerMeta.speakerTimezoneLabel).toBeUndefined();
|
|
348
355
|
});
|
|
349
356
|
|
|
350
357
|
test("falls back to the turn client timezone when no configured user timezone is set", async () => {
|
|
@@ -364,14 +371,14 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
364
371
|
);
|
|
365
372
|
|
|
366
373
|
const persisted = lastAssistantPersisted();
|
|
367
|
-
const
|
|
368
|
-
persisted.metadata?.
|
|
374
|
+
const providerMeta = JSON.parse(
|
|
375
|
+
persisted.metadata?.providerMeta as string,
|
|
369
376
|
) as Record<string, unknown>;
|
|
370
|
-
expect(
|
|
371
|
-
expect(
|
|
377
|
+
expect(providerMeta.timestampTimezone).toBe("America/Los_Angeles");
|
|
378
|
+
expect(providerMeta.timestampTimezoneLabel).toBe("PT");
|
|
372
379
|
});
|
|
373
380
|
|
|
374
|
-
test("post-send reconciliation
|
|
381
|
+
test("post-send reconciliation gives the row its Slack view, timezone fields intact", async () => {
|
|
375
382
|
setConfig("ui", { userTimezone: "America/Denver" });
|
|
376
383
|
const conversationId = "conv-slack-reconcile-timezone";
|
|
377
384
|
const channelId = "C999RECONCILE";
|
|
@@ -389,9 +396,9 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
389
396
|
);
|
|
390
397
|
|
|
391
398
|
const persisted = lastAssistantPersisted();
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
399
|
+
expect(
|
|
400
|
+
readSlackMetadataFromMessageMetadata(JSON.stringify(persisted.metadata)),
|
|
401
|
+
).toBeNull();
|
|
395
402
|
|
|
396
403
|
nextDeliveryTs = "1772678280.000200";
|
|
397
404
|
await deliverReplyViaCallback(
|
|
@@ -406,19 +413,28 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
406
413
|
(candidate) => candidate.id === persisted.id,
|
|
407
414
|
);
|
|
408
415
|
expect(typeof row?.metadata).toBe("string");
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
expect(
|
|
412
|
-
expect(
|
|
413
|
-
expect(
|
|
414
|
-
expect(
|
|
415
|
-
expect(
|
|
416
|
+
// The Slack renderers read the reconciled row through its Slack view.
|
|
417
|
+
const view = readSlackMetadataFromMessageMetadata(row!.metadata!);
|
|
418
|
+
expect(view).not.toBeNull();
|
|
419
|
+
expect(view!.channelId).toBe(channelId);
|
|
420
|
+
expect(view!.channelTs).toBe("1772678280.000200");
|
|
421
|
+
expect(view!.timestampTimezone).toBe("America/Denver");
|
|
422
|
+
expect(view!.timestampTimezoneLabel).toBe("MT");
|
|
423
|
+
expect(view!.speakerTimezoneLabel).toBeUndefined();
|
|
424
|
+
// And the post is in the outbound index, exactly as any channel's is.
|
|
425
|
+
expect(recordedOutboundPosts).toContainEqual({
|
|
426
|
+
sourceChannel: "slack",
|
|
427
|
+
externalChatId: channelId,
|
|
428
|
+
providerMessageId: "1772678280.000200",
|
|
429
|
+
messageId: persisted.id,
|
|
430
|
+
conversationId,
|
|
431
|
+
});
|
|
416
432
|
});
|
|
417
433
|
|
|
418
|
-
test("stamps
|
|
434
|
+
test("stamps no threadId for top-level Slack replies", async () => {
|
|
419
435
|
const conversationId = "conv-slack-toplevel";
|
|
420
436
|
const channelId = "C456NOTHREAD";
|
|
421
|
-
// The turn arrived at the channel root
|
|
437
|
+
// The turn arrived at the channel root: no `sourceThreadId` on the trust
|
|
422
438
|
// context, so the reply targets the channel root, not a thread.
|
|
423
439
|
|
|
424
440
|
const deps = makeDeps(conversationId, {
|
|
@@ -429,20 +445,14 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
429
445
|
await handleMessageComplete(state, deps, makeMessageCompleteEvent("hello"));
|
|
430
446
|
|
|
431
447
|
const persisted = lastAssistantPersisted();
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
expect(
|
|
440
|
-
expect(slackMeta.eventKind).toBe("message");
|
|
441
|
-
expect(slackMeta.channelId).toBe(channelId);
|
|
442
|
-
// threadTs is intentionally absent — top-level reply.
|
|
443
|
-
expect(slackMeta.threadTs).toBeUndefined();
|
|
444
|
-
// channelTs is still absent for the same persistence-vs-send reason.
|
|
445
|
-
expect(slackMeta.channelTs).toBeUndefined();
|
|
448
|
+
const providerMeta = JSON.parse(
|
|
449
|
+
persisted.metadata?.providerMeta as string,
|
|
450
|
+
) as Record<string, unknown>;
|
|
451
|
+
expect(providerMeta.source).toBe("slack");
|
|
452
|
+
expect(providerMeta.eventKind).toBe("message");
|
|
453
|
+
expect(providerMeta.conversationExternalId).toBe(channelId);
|
|
454
|
+
expect(providerMeta.threadId).toBeUndefined();
|
|
455
|
+
expect(providerMeta.messageId).toBeUndefined();
|
|
446
456
|
});
|
|
447
457
|
|
|
448
458
|
test("envelope resolves from the turn's actor when the slot has moved", async () => {
|
|
@@ -471,17 +481,17 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
471
481
|
|
|
472
482
|
const persisted = lastAssistantPersisted();
|
|
473
483
|
expect(persisted.metadata?.provenanceTrustClass).toBe("trusted_contact");
|
|
474
|
-
const
|
|
475
|
-
persisted.metadata?.
|
|
484
|
+
const providerMeta = JSON.parse(
|
|
485
|
+
persisted.metadata?.providerMeta as string,
|
|
476
486
|
) as Record<string, unknown>;
|
|
477
|
-
expect(
|
|
478
|
-
expect(
|
|
487
|
+
expect(providerMeta.conversationExternalId).toBe("C-CONTACT");
|
|
488
|
+
expect(providerMeta.threadId).toBe("1723300000.000100");
|
|
479
489
|
});
|
|
480
490
|
|
|
481
491
|
test("ignores a non-ts sourceThreadId", async () => {
|
|
482
492
|
const conversationId = "conv-slack-bad-thread";
|
|
483
493
|
const channelId = "C789BAD";
|
|
484
|
-
// A malformed thread id must not be stamped as a
|
|
494
|
+
// A malformed thread id must not be stamped as a thread (isSlackTs guard).
|
|
485
495
|
const deps = makeDeps(conversationId, {
|
|
486
496
|
assistantMessageChannel: "slack",
|
|
487
497
|
requesterChatId: channelId,
|
|
@@ -495,13 +505,10 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
495
505
|
);
|
|
496
506
|
|
|
497
507
|
const persisted = lastAssistantPersisted();
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
unknown
|
|
503
|
-
>;
|
|
504
|
-
expect(slackMeta.threadTs).toBeUndefined();
|
|
508
|
+
const providerMeta = JSON.parse(
|
|
509
|
+
persisted.metadata?.providerMeta as string,
|
|
510
|
+
) as Record<string, unknown>;
|
|
511
|
+
expect(providerMeta.threadId).toBeUndefined();
|
|
505
512
|
});
|
|
506
513
|
|
|
507
514
|
test("does NOT stamp slackMeta on non-Slack outbound assistant messages", async () => {
|
|
@@ -613,6 +620,12 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
613
620
|
) as Record<string, unknown>;
|
|
614
621
|
expect(metaAfter.messageId).toBe("1234567890123456789");
|
|
615
622
|
expect(metaAfter.additionalMessageIds).toEqual(["8888888888888888888"]);
|
|
623
|
+
// Both ids also land in the channel_outbound_posts resolution index,
|
|
624
|
+
// written by the same reconciliation so envelope and index cannot drift.
|
|
625
|
+
expect(recordedOutboundPosts.map((post) => post.providerMessageId)).toEqual(
|
|
626
|
+
["1234567890123456789", "8888888888888888888"],
|
|
627
|
+
);
|
|
628
|
+
expect(recordedOutboundPosts[0].messageId).toBe(persisted.id);
|
|
616
629
|
|
|
617
630
|
// A redelivery reporting an already-recorded id changes nothing.
|
|
618
631
|
nextDeliveryTs = "8888888888888888888";
|
|
@@ -1,111 +1,129 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* How `persistUserMessage` takes and gives back the conversation's processing
|
|
3
|
+
* flag.
|
|
4
|
+
*
|
|
5
|
+
* It acquires rather than sets: the read and the take are one step, and the
|
|
6
|
+
* hold it takes is released only by the claim that took it. That is what keeps
|
|
7
|
+
* a turn that is starting from publishing an idle conversation, and what keeps
|
|
8
|
+
* an earlier holder from releasing a turn that claimed the flag away.
|
|
9
|
+
*
|
|
10
|
+
* A failure anywhere after the acquire must unwind the request-id and
|
|
11
|
+
* abort-controller bookkeeping rather than stranding it on the conversation,
|
|
12
|
+
* and a failure while releasing must not mask the original error.
|
|
7
13
|
*/
|
|
8
14
|
import { describe, expect, test } from "bun:test";
|
|
9
15
|
|
|
10
16
|
import type { MessagingConversationContext } from "../daemon/conversation-messaging.js";
|
|
11
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
CONVERSATION_BUSY_MESSAGE,
|
|
19
|
+
persistUserMessage,
|
|
20
|
+
} from "../daemon/conversation-messaging.js";
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
interface ContextBehavior {
|
|
23
|
+
/** Answered in place of taking the flag. `undefined` takes it for real. */
|
|
24
|
+
acquire?: () => Promise<number | null>;
|
|
25
|
+
/** Runs before the release completes, so it can throw. */
|
|
26
|
+
onRelease?: (owner: number) => void;
|
|
27
|
+
}
|
|
14
28
|
|
|
15
|
-
function makeContext(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
function makeContext(behavior: ContextBehavior = {}): {
|
|
30
|
+
ctx: MessagingConversationContext;
|
|
31
|
+
processing: () => boolean;
|
|
32
|
+
releases: number[];
|
|
19
33
|
} {
|
|
20
34
|
let processing = false;
|
|
35
|
+
let owner = 0;
|
|
36
|
+
const releases: number[] = [];
|
|
21
37
|
const ctx = {
|
|
22
|
-
conversationId: "conv-
|
|
38
|
+
conversationId: "conv-processing-ownership",
|
|
23
39
|
messages: [],
|
|
24
40
|
abortController: null,
|
|
25
41
|
currentRequestId: undefined,
|
|
26
42
|
queue: {} as never,
|
|
27
|
-
get processing() {
|
|
28
|
-
return processing;
|
|
29
|
-
},
|
|
30
43
|
isProcessing: () => processing,
|
|
31
44
|
setProcessing: (value: boolean) => {
|
|
32
|
-
setProcessing(value);
|
|
33
45
|
processing = value;
|
|
34
46
|
},
|
|
47
|
+
acquireProcessingFenced: async () => {
|
|
48
|
+
if (behavior.acquire) {
|
|
49
|
+
return behavior.acquire();
|
|
50
|
+
}
|
|
51
|
+
if (processing) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
processing = true;
|
|
55
|
+
owner += 1;
|
|
56
|
+
return owner;
|
|
57
|
+
},
|
|
58
|
+
releaseProcessing: (claim: number) => {
|
|
59
|
+
releases.push(claim);
|
|
60
|
+
behavior.onRelease?.(claim);
|
|
61
|
+
if (claim !== owner) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
processing = false;
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
35
67
|
getTurnChannelContext: () => null,
|
|
36
68
|
getTurnInterfaceContext: () => null,
|
|
37
|
-
} as unknown as MessagingConversationContext
|
|
38
|
-
return ctx;
|
|
69
|
+
} as unknown as MessagingConversationContext;
|
|
70
|
+
return { ctx, processing: () => processing, releases };
|
|
39
71
|
}
|
|
40
72
|
|
|
41
|
-
describe("persistUserMessage
|
|
42
|
-
test("
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
});
|
|
73
|
+
describe("persistUserMessage processing ownership", () => {
|
|
74
|
+
test("unwinds its bookkeeping and its hold when the persist fails", async () => {
|
|
75
|
+
// No database stands behind this context, so the body throws. The flag it
|
|
76
|
+
// took has to come back with it.
|
|
77
|
+
const { ctx, processing, releases } = makeContext();
|
|
48
78
|
|
|
49
79
|
await expect(
|
|
50
80
|
persistUserMessage(ctx, { content: "hello", requestId: "req-1" }),
|
|
51
|
-
).rejects.toThrow(
|
|
81
|
+
).rejects.toThrow();
|
|
52
82
|
|
|
53
83
|
expect(ctx.currentRequestId).toBeUndefined();
|
|
54
84
|
expect(ctx.abortController).toBeNull();
|
|
55
|
-
expect(
|
|
85
|
+
expect(processing()).toBe(false);
|
|
86
|
+
expect(releases).toEqual([1]);
|
|
56
87
|
});
|
|
57
88
|
|
|
58
|
-
test("
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// re-attempted rather than failing the send immediately.
|
|
62
|
-
let trueCalls = 0;
|
|
63
|
-
const ctx = makeContext((value) => {
|
|
64
|
-
if (value) {
|
|
65
|
-
trueCalls += 1;
|
|
66
|
-
throw Object.assign(new Error("database is locked"), {
|
|
67
|
-
code: "SQLITE_BUSY",
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
});
|
|
89
|
+
test("refuses a conversation another holder is already processing", async () => {
|
|
90
|
+
const { ctx, releases } = makeContext();
|
|
91
|
+
ctx.setProcessing(true);
|
|
71
92
|
|
|
72
93
|
await expect(
|
|
73
94
|
persistUserMessage(ctx, { content: "hello", requestId: "req-busy" }),
|
|
74
|
-
).rejects.toThrow(
|
|
95
|
+
).rejects.toThrow(CONVERSATION_BUSY_MESSAGE);
|
|
75
96
|
|
|
76
|
-
//
|
|
77
|
-
expect(
|
|
78
|
-
expect(ctx.currentRequestId).toBeUndefined();
|
|
79
|
-
expect(ctx.abortController).toBeNull();
|
|
97
|
+
// Nothing was taken, so nothing is given back.
|
|
98
|
+
expect(releases).toEqual([]);
|
|
80
99
|
});
|
|
81
100
|
|
|
82
|
-
test("
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
const ctx = makeContext((
|
|
86
|
-
if (value) {
|
|
87
|
-
trueCalls += 1;
|
|
88
|
-
throw new Error("boom");
|
|
89
|
-
}
|
|
90
|
-
});
|
|
101
|
+
test("reports busy rather than stealing a flag the acquire finds taken", async () => {
|
|
102
|
+
// The acquire is the only thing that decides: a caller cannot set its way
|
|
103
|
+
// past a holder.
|
|
104
|
+
const { ctx, releases } = makeContext({ acquire: async () => null });
|
|
91
105
|
|
|
92
106
|
await expect(
|
|
93
|
-
persistUserMessage(ctx, { content: "hello", requestId: "req-
|
|
94
|
-
).rejects.toThrow(
|
|
107
|
+
persistUserMessage(ctx, { content: "hello", requestId: "req-taken" }),
|
|
108
|
+
).rejects.toThrow(CONVERSATION_BUSY_MESSAGE);
|
|
95
109
|
|
|
96
|
-
expect(
|
|
110
|
+
expect(ctx.currentRequestId).toBeUndefined();
|
|
111
|
+
expect(ctx.abortController).toBeNull();
|
|
112
|
+
expect(releases).toEqual([]);
|
|
97
113
|
});
|
|
98
114
|
|
|
99
|
-
test("a failing
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
test("a failing release does not mask the original error", async () => {
|
|
116
|
+
const { ctx } = makeContext({
|
|
117
|
+
onRelease: () => {
|
|
118
|
+
throw new Error("release exploded");
|
|
119
|
+
},
|
|
104
120
|
});
|
|
105
121
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
const attempt = persistUserMessage(ctx, {
|
|
123
|
+
content: "hello",
|
|
124
|
+
requestId: "req-2",
|
|
125
|
+
});
|
|
126
|
+
await expect(attempt).rejects.not.toThrow("release exploded");
|
|
109
127
|
|
|
110
128
|
expect(ctx.currentRequestId).toBeUndefined();
|
|
111
129
|
expect(ctx.abortController).toBeNull();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
let platformAssistantId = "asst_123";
|
|
4
|
+
let fetchImpl: (
|
|
5
|
+
url: string,
|
|
6
|
+
init?: RequestInit,
|
|
7
|
+
) => Promise<Response> = async () => new Response("{}", { status: 200 });
|
|
8
|
+
const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
|
|
9
|
+
|
|
10
|
+
mock.module("../platform/feature-gate.js", () => ({
|
|
11
|
+
arePlatformFeaturesEnabled: () => true,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module("../providers/platform-proxy/context.js", () => ({
|
|
15
|
+
resolveManagedProxyContext: async () => ({
|
|
16
|
+
enabled: true,
|
|
17
|
+
platformBaseUrl: "https://platform.test/",
|
|
18
|
+
assistantApiKey: "assistant-key",
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module("../config/env.js", () => ({
|
|
23
|
+
getPlatformAssistantId: () => platformAssistantId,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
27
|
+
getSecureKeyAsync: async () => null,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const { VellumPlatformClient } = await import("../platform/client.js");
|
|
31
|
+
|
|
32
|
+
const originalFetch = globalThis.fetch;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
platformAssistantId = "asst_123";
|
|
36
|
+
fetchCalls.length = 0;
|
|
37
|
+
fetchImpl = async () => new Response("{}", { status: 200 });
|
|
38
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
39
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
40
|
+
fetchCalls.push({ url, init });
|
|
41
|
+
return fetchImpl(url, init);
|
|
42
|
+
}) as typeof fetch;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
globalThis.fetch = originalFetch;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
async function createClient() {
|
|
50
|
+
const client = await VellumPlatformClient.create();
|
|
51
|
+
if (!client) {
|
|
52
|
+
throw new Error("client prerequisites were not satisfied");
|
|
53
|
+
}
|
|
54
|
+
return client;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("VellumPlatformClient.verifyCredential", () => {
|
|
58
|
+
test("an accepted request is a valid credential, asked on the owner-consent carrier with the stored key", async () => {
|
|
59
|
+
const client = await createClient();
|
|
60
|
+
|
|
61
|
+
expect(await client.verifyCredential()).toBe("valid");
|
|
62
|
+
|
|
63
|
+
expect(fetchCalls).toHaveLength(1);
|
|
64
|
+
expect(fetchCalls[0]?.url).toBe(
|
|
65
|
+
"https://platform.test/v1/assistants/asst_123/owner-consent/",
|
|
66
|
+
);
|
|
67
|
+
const headers = new Headers(fetchCalls[0]?.init?.headers);
|
|
68
|
+
expect(headers.get("Authorization")).toBe("Api-Key assistant-key");
|
|
69
|
+
// The heartbeat awaits this check, so it must carry a deadline.
|
|
70
|
+
expect(fetchCalls[0]?.init?.signal).toBeInstanceOf(AbortSignal);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Only the platform's own refusal may drive a rotation.
|
|
74
|
+
test.each([401, 403])("a %i is the settled rejection", async (status) => {
|
|
75
|
+
fetchImpl = async () => new Response("", { status });
|
|
76
|
+
const client = await createClient();
|
|
77
|
+
expect(await client.verifyCredential()).toBe("rejected");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a server error is unsettled, not a rejection", async () => {
|
|
81
|
+
fetchImpl = async () => new Response("", { status: 503 });
|
|
82
|
+
const client = await createClient();
|
|
83
|
+
expect(await client.verifyCredential()).toBe("unknown");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("an unreachable platform is unsettled, not a rejection", async () => {
|
|
87
|
+
fetchImpl = async () => {
|
|
88
|
+
throw new TypeError("fetch failed");
|
|
89
|
+
};
|
|
90
|
+
const client = await createClient();
|
|
91
|
+
expect(await client.verifyCredential()).toBe("unknown");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("without a platform assistant id there is nothing to verify", async () => {
|
|
95
|
+
platformAssistantId = "";
|
|
96
|
+
const client = await createClient();
|
|
97
|
+
expect(await client.verifyCredential()).toBe("unknown");
|
|
98
|
+
expect(fetchCalls).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|