@vellumai/assistant 0.11.1 → 0.11.2-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import type { FeedItem } from "../../home/feed-types.js";
|
|
7
|
+
import type { NotificationDeliveryRow } from "../deliveries-store.js";
|
|
8
|
+
|
|
9
|
+
// ── Module mocks ───────────────────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// The home-feed writer is deliberately NOT mocked: these tests exercise
|
|
12
|
+
// the real on-disk patch semantics through a temp workspace so the
|
|
13
|
+
// "an empty title never clears the field" guarantee is covered end to
|
|
14
|
+
// end. Only the sqlite-backed stores, the broadcaster, and the SSE hub
|
|
15
|
+
// are intercepted.
|
|
16
|
+
|
|
17
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
18
|
+
assistantEventHub: {
|
|
19
|
+
publish: async () => {},
|
|
20
|
+
subscribe: () => () => {},
|
|
21
|
+
},
|
|
22
|
+
broadcastMessage: () => {},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
let decisionRow: { id: string } | null = null;
|
|
26
|
+
let deliveryRows: NotificationDeliveryRow[] = [];
|
|
27
|
+
const renderedCopyPatches: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
patch: { renderedTitle?: string; renderedBody?: string };
|
|
30
|
+
}> = [];
|
|
31
|
+
|
|
32
|
+
mock.module("../decisions-store.js", () => ({
|
|
33
|
+
findLatestDecisionByEventId: () => decisionRow,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
mock.module("../deliveries-store.js", () => ({
|
|
37
|
+
findDeliveriesByDecisionId: () => deliveryRows,
|
|
38
|
+
updateDeliveryRenderedCopy: (
|
|
39
|
+
id: string,
|
|
40
|
+
patch: { renderedTitle?: string; renderedBody?: string },
|
|
41
|
+
) => {
|
|
42
|
+
renderedCopyPatches.push({ id, patch });
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const adapterUpdates: Array<{ title?: string; body?: string }> = [];
|
|
48
|
+
let adapterSupportsUpdate = true;
|
|
49
|
+
|
|
50
|
+
mock.module("../emit-signal.js", () => ({
|
|
51
|
+
getBroadcaster: () => ({
|
|
52
|
+
getAdapter: () =>
|
|
53
|
+
adapterSupportsUpdate
|
|
54
|
+
? {
|
|
55
|
+
update: async (
|
|
56
|
+
_target: unknown,
|
|
57
|
+
patch: { title?: string; body?: string },
|
|
58
|
+
) => {
|
|
59
|
+
adapterUpdates.push(patch);
|
|
60
|
+
return { success: true };
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
: {},
|
|
64
|
+
}),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const { appendFeedItem, readHomeFeed } =
|
|
68
|
+
await import("../../home/feed-writer.js");
|
|
69
|
+
const { editNotification, normalizeFeedItemId, feedItemIdToSignalId } =
|
|
70
|
+
await import("../edit-notification.js");
|
|
71
|
+
|
|
72
|
+
// ── Fixtures ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const SIGNAL_ID = "sig-1";
|
|
75
|
+
const FEED_ITEM_ID = `notif:${SIGNAL_ID}`;
|
|
76
|
+
|
|
77
|
+
function makeItem(overrides: Partial<FeedItem> = {}): FeedItem {
|
|
78
|
+
return {
|
|
79
|
+
id: FEED_ITEM_ID,
|
|
80
|
+
type: "notification",
|
|
81
|
+
priority: 50,
|
|
82
|
+
title: "Backup complete",
|
|
83
|
+
summary: "Nightly backup finished",
|
|
84
|
+
timestamp: "2026-04-14T12:00:00.000Z",
|
|
85
|
+
status: "new",
|
|
86
|
+
createdAt: "2026-04-14T12:00:00.000Z",
|
|
87
|
+
...overrides,
|
|
88
|
+
} as FeedItem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function makeDelivery(
|
|
92
|
+
overrides: Partial<NotificationDeliveryRow> = {},
|
|
93
|
+
): NotificationDeliveryRow {
|
|
94
|
+
return {
|
|
95
|
+
id: "del-1",
|
|
96
|
+
notificationDecisionId: "dec-1",
|
|
97
|
+
channel: "slack",
|
|
98
|
+
destination: "C123",
|
|
99
|
+
status: "sent",
|
|
100
|
+
attempt: 1,
|
|
101
|
+
renderedTitle: "Backup complete",
|
|
102
|
+
renderedBody: "Nightly backup finished",
|
|
103
|
+
errorCode: null,
|
|
104
|
+
errorMessage: null,
|
|
105
|
+
sentAt: 1700000000000,
|
|
106
|
+
conversationId: null,
|
|
107
|
+
messageId: "1700000000.0001",
|
|
108
|
+
conversationStrategy: null,
|
|
109
|
+
conversationAction: null,
|
|
110
|
+
conversationTargetId: null,
|
|
111
|
+
conversationFallbackUsed: null,
|
|
112
|
+
clientDeliveryStatus: null,
|
|
113
|
+
clientDeliveryError: null,
|
|
114
|
+
clientDeliveryAt: null,
|
|
115
|
+
createdAt: 1700000000000,
|
|
116
|
+
updatedAt: 1700000000000,
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readItem(id = FEED_ITEM_ID): FeedItem | undefined {
|
|
122
|
+
return readHomeFeed().items.find((item) => item.id === id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let workspaceDir: string;
|
|
126
|
+
let origWorkspaceDir: string | undefined;
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-edit-notif-"));
|
|
130
|
+
origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
131
|
+
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
132
|
+
decisionRow = null;
|
|
133
|
+
deliveryRows = [];
|
|
134
|
+
renderedCopyPatches.length = 0;
|
|
135
|
+
adapterUpdates.length = 0;
|
|
136
|
+
adapterSupportsUpdate = true;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
afterEach(() => {
|
|
140
|
+
if (origWorkspaceDir === undefined) {
|
|
141
|
+
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
142
|
+
} else {
|
|
143
|
+
process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
147
|
+
} catch {
|
|
148
|
+
// best-effort
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── Tests ──────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe("normalizeFeedItemId", () => {
|
|
155
|
+
test("adds the notif: prefix to a bare uuid and leaves prefixed ids alone", () => {
|
|
156
|
+
expect(normalizeFeedItemId(SIGNAL_ID)).toBe(FEED_ITEM_ID);
|
|
157
|
+
expect(normalizeFeedItemId(` ${FEED_ITEM_ID} `)).toBe(FEED_ITEM_ID);
|
|
158
|
+
expect(feedItemIdToSignalId(FEED_ITEM_ID)).toBe(SIGNAL_ID);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("editNotification", () => {
|
|
163
|
+
test("applies a title edit to the feed item", async () => {
|
|
164
|
+
await appendFeedItem(makeItem());
|
|
165
|
+
|
|
166
|
+
const result = await editNotification({
|
|
167
|
+
id: FEED_ITEM_ID,
|
|
168
|
+
title: "Backup finished early",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result).not.toBeNull();
|
|
172
|
+
expect(result!.feedItem.title).toBe("Backup finished early");
|
|
173
|
+
expect(readItem()!.title).toBe("Backup finished early");
|
|
174
|
+
expect(readItem()!.summary).toBe("Nightly backup finished");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("accepts a bare signal uuid as the id", async () => {
|
|
178
|
+
await appendFeedItem(makeItem());
|
|
179
|
+
|
|
180
|
+
const result = await editNotification({
|
|
181
|
+
id: SIGNAL_ID,
|
|
182
|
+
title: "Renamed via bare id",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(result!.feedItem.id).toBe(FEED_ITEM_ID);
|
|
186
|
+
expect(readItem()!.title).toBe("Renamed via bare id");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an empty title edit leaves the previous title intact", async () => {
|
|
190
|
+
await appendFeedItem(makeItem());
|
|
191
|
+
|
|
192
|
+
const result = await editNotification({ id: FEED_ITEM_ID, title: "" });
|
|
193
|
+
|
|
194
|
+
expect(result).not.toBeNull();
|
|
195
|
+
expect(result!.feedItem.title).toBe("Backup complete");
|
|
196
|
+
expect(readItem()!.title).toBe("Backup complete");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a whitespace-only title edit leaves the previous title intact", async () => {
|
|
200
|
+
await appendFeedItem(makeItem());
|
|
201
|
+
|
|
202
|
+
const result = await editNotification({ id: FEED_ITEM_ID, title: " \t" });
|
|
203
|
+
|
|
204
|
+
expect(result!.feedItem.title).toBe("Backup complete");
|
|
205
|
+
expect(readItem()!.title).toBe("Backup complete");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("an empty title edit does not push a channel update", async () => {
|
|
209
|
+
await appendFeedItem(makeItem());
|
|
210
|
+
decisionRow = { id: "dec-1" };
|
|
211
|
+
deliveryRows = [makeDelivery()];
|
|
212
|
+
|
|
213
|
+
const result = await editNotification({ id: FEED_ITEM_ID, title: "" });
|
|
214
|
+
|
|
215
|
+
expect(result!.channels).toEqual([]);
|
|
216
|
+
expect(adapterUpdates).toHaveLength(0);
|
|
217
|
+
expect(renderedCopyPatches).toHaveLength(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("a body-only edit does not disturb the title", async () => {
|
|
221
|
+
await appendFeedItem(makeItem());
|
|
222
|
+
|
|
223
|
+
const result = await editNotification({
|
|
224
|
+
id: FEED_ITEM_ID,
|
|
225
|
+
body: "Nightly backup finished in 4 minutes",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result!.feedItem.title).toBe("Backup complete");
|
|
229
|
+
expect(result!.feedItem.summary).toBe(
|
|
230
|
+
"Nightly backup finished in 4 minutes",
|
|
231
|
+
);
|
|
232
|
+
expect(readItem()!.title).toBe("Backup complete");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("a feed-only edit skips channel updates entirely", async () => {
|
|
236
|
+
await appendFeedItem(makeItem());
|
|
237
|
+
decisionRow = { id: "dec-1" };
|
|
238
|
+
deliveryRows = [makeDelivery()];
|
|
239
|
+
|
|
240
|
+
const result = await editNotification({
|
|
241
|
+
id: FEED_ITEM_ID,
|
|
242
|
+
status: "dismissed",
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(result!.feedItem.status).toBe("dismissed");
|
|
246
|
+
expect(result!.channels).toEqual([]);
|
|
247
|
+
expect(adapterUpdates).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("returns null for an unknown id", async () => {
|
|
251
|
+
await appendFeedItem(makeItem());
|
|
252
|
+
|
|
253
|
+
const result = await editNotification({
|
|
254
|
+
id: "notif:does-not-exist",
|
|
255
|
+
title: "Nope",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(result).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("pushes the new copy to update-capable channel deliveries", async () => {
|
|
262
|
+
await appendFeedItem(makeItem());
|
|
263
|
+
decisionRow = { id: "dec-1" };
|
|
264
|
+
deliveryRows = [makeDelivery()];
|
|
265
|
+
|
|
266
|
+
const result = await editNotification({
|
|
267
|
+
id: FEED_ITEM_ID,
|
|
268
|
+
title: "Backup finished early",
|
|
269
|
+
body: "Done in 4 minutes",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(adapterUpdates).toEqual([
|
|
273
|
+
{ title: "Backup finished early", body: "Done in 4 minutes" },
|
|
274
|
+
]);
|
|
275
|
+
expect(renderedCopyPatches).toEqual([
|
|
276
|
+
{
|
|
277
|
+
id: "del-1",
|
|
278
|
+
patch: {
|
|
279
|
+
renderedTitle: "Backup finished early",
|
|
280
|
+
renderedBody: "Done in 4 minutes",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
expect(result!.channels).toEqual([
|
|
285
|
+
{ channel: "slack", deliveryId: "del-1", outcome: "updated" },
|
|
286
|
+
]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("reports channels whose adapter cannot edit in place as unsupported", async () => {
|
|
290
|
+
await appendFeedItem(makeItem());
|
|
291
|
+
decisionRow = { id: "dec-1" };
|
|
292
|
+
deliveryRows = [makeDelivery()];
|
|
293
|
+
adapterSupportsUpdate = false;
|
|
294
|
+
|
|
295
|
+
const result = await editNotification({
|
|
296
|
+
id: FEED_ITEM_ID,
|
|
297
|
+
title: "Backup finished early",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(result!.channels[0]!.outcome).toBe("unsupported");
|
|
301
|
+
expect(renderedCopyPatches).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("skips deliveries that never reached sent", async () => {
|
|
305
|
+
await appendFeedItem(makeItem());
|
|
306
|
+
decisionRow = { id: "dec-1" };
|
|
307
|
+
deliveryRows = [makeDelivery({ status: "failed" })];
|
|
308
|
+
|
|
309
|
+
const result = await editNotification({
|
|
310
|
+
id: FEED_ITEM_ID,
|
|
311
|
+
title: "Backup finished early",
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result!.channels[0]!.outcome).toBe("skipped");
|
|
315
|
+
expect(adapterUpdates).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("skips channel updates when the feed item has no persisted decision", async () => {
|
|
319
|
+
await appendFeedItem(makeItem());
|
|
320
|
+
decisionRow = null;
|
|
321
|
+
|
|
322
|
+
const result = await editNotification({
|
|
323
|
+
id: FEED_ITEM_ID,
|
|
324
|
+
title: "Backup finished early",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result!.channels).toEqual([]);
|
|
328
|
+
expect(adapterUpdates).toHaveLength(0);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const createGuardianRequestDeliveryMock = mock();
|
|
4
|
+
const updateGuardianRequestDeliveryMock = mock();
|
|
5
|
+
|
|
6
|
+
mock.module("../../channels/gateway-guardian-requests.js", () => ({
|
|
7
|
+
createGuardianRequestDelivery: (...args: unknown[]) =>
|
|
8
|
+
createGuardianRequestDeliveryMock(...args),
|
|
9
|
+
updateGuardianRequestDelivery: (...args: unknown[]) =>
|
|
10
|
+
updateGuardianRequestDeliveryMock(...args),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { recordGuardianRequestDeliveries } from "../guardian-delivery-recorder.js";
|
|
14
|
+
import type { NotificationDeliveryResult } from "../types.js";
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
createGuardianRequestDeliveryMock.mockReset();
|
|
18
|
+
updateGuardianRequestDeliveryMock.mockReset();
|
|
19
|
+
createGuardianRequestDeliveryMock.mockImplementation(async () => ({
|
|
20
|
+
id: `row-${createGuardianRequestDeliveryMock.mock.calls.length}`,
|
|
21
|
+
}));
|
|
22
|
+
updateGuardianRequestDeliveryMock.mockResolvedValue(undefined);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("recordGuardianRequestDeliveries", () => {
|
|
26
|
+
test("records rows only for addressable channels, skipping platform", async () => {
|
|
27
|
+
const deliveryResults: NotificationDeliveryResult[] = [
|
|
28
|
+
{
|
|
29
|
+
channel: "vellum",
|
|
30
|
+
destination: "vellum",
|
|
31
|
+
status: "sent",
|
|
32
|
+
conversationId: "conv-1",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
// A forced platform push has no endpoint, so the broadcaster falls
|
|
36
|
+
// back to the channel name as the destination -- it must never be
|
|
37
|
+
// persisted as a destinationChatId.
|
|
38
|
+
channel: "platform",
|
|
39
|
+
destination: "platform",
|
|
40
|
+
status: "pending",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
channel: "telegram",
|
|
44
|
+
destination: "12345",
|
|
45
|
+
status: "sent",
|
|
46
|
+
conversationId: "conv-1",
|
|
47
|
+
messageId: "tg-77",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
await recordGuardianRequestDeliveries({
|
|
52
|
+
requestId: "req-1",
|
|
53
|
+
deliveryResults,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const channels = createGuardianRequestDeliveryMock.mock.calls.map(
|
|
57
|
+
(call) => (call[0] as { destinationChannel: string }).destinationChannel,
|
|
58
|
+
);
|
|
59
|
+
expect(channels).toEqual(["vellum", "telegram"]);
|
|
60
|
+
|
|
61
|
+
const telegramRow = createGuardianRequestDeliveryMock.mock.calls[1]![0] as {
|
|
62
|
+
destinationChatId?: string;
|
|
63
|
+
destinationMessageId?: string;
|
|
64
|
+
};
|
|
65
|
+
expect(telegramRow.destinationChatId).toBe("12345");
|
|
66
|
+
expect(telegramRow.destinationMessageId).toBe("tg-77");
|
|
67
|
+
|
|
68
|
+
// Status patches apply only to the rows that were created.
|
|
69
|
+
expect(updateGuardianRequestDeliveryMock).toHaveBeenCalledTimes(2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -320,11 +320,9 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
320
320
|
expect(appendCalls).toHaveLength(0);
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
-
test("
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
// copy echoes the body into `renderedCopy.title`). Leave `title`
|
|
327
|
-
// undefined so renderers fall back to `summary` instead of stuttering.
|
|
323
|
+
test("derives a title from the summary when only the body is available", async () => {
|
|
324
|
+
// With no authored candidate at all, the title is derived from the
|
|
325
|
+
// summary rather than left off: every row carries a headline.
|
|
328
326
|
conversationRow = { conversationType: "background" };
|
|
329
327
|
const signal = makeSignal({
|
|
330
328
|
sourceEventName: "example.event",
|
|
@@ -335,14 +333,14 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
335
333
|
|
|
336
334
|
expect(item).not.toBeNull();
|
|
337
335
|
expect(appendCalls).toHaveLength(1);
|
|
338
|
-
expect(appendCalls[0]!.title).
|
|
336
|
+
expect(appendCalls[0]!.title).toBe("Real body");
|
|
339
337
|
expect(appendCalls[0]!.summary).toBe("Real body");
|
|
340
338
|
});
|
|
341
339
|
|
|
342
|
-
test("
|
|
343
|
-
// The
|
|
344
|
-
//
|
|
345
|
-
//
|
|
340
|
+
test("uses the LLM-rendered title when no payload title was supplied", async () => {
|
|
341
|
+
// The model authors `renderedCopy.title` as a topic headline, and the
|
|
342
|
+
// decision engine validates it before it gets here, so it is the second
|
|
343
|
+
// candidate after the payload title.
|
|
346
344
|
conversationRow = { conversationType: "background" };
|
|
347
345
|
const signal = makeSignal({
|
|
348
346
|
sourceEventName: "example.event",
|
|
@@ -351,7 +349,7 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
351
349
|
const decision = makeDecision({
|
|
352
350
|
renderedCopy: {
|
|
353
351
|
vellum: {
|
|
354
|
-
title: "
|
|
352
|
+
title: "Nightly sync finished",
|
|
355
353
|
body: "Real body",
|
|
356
354
|
},
|
|
357
355
|
},
|
|
@@ -361,10 +359,170 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
361
359
|
|
|
362
360
|
expect(item).not.toBeNull();
|
|
363
361
|
expect(appendCalls).toHaveLength(1);
|
|
364
|
-
expect(appendCalls[0]!.title).
|
|
362
|
+
expect(appendCalls[0]!.title).toBe("Nightly sync finished");
|
|
365
363
|
expect(appendCalls[0]!.summary).toBe("Real body");
|
|
366
364
|
});
|
|
367
365
|
|
|
366
|
+
test("payload title wins over the rendered title when it is clean", async () => {
|
|
367
|
+
conversationRow = { conversationType: "background" };
|
|
368
|
+
const signal = makeSignal({
|
|
369
|
+
sourceEventName: "example.event",
|
|
370
|
+
contextPayload: { title: "Payload headline" },
|
|
371
|
+
});
|
|
372
|
+
const decision = makeDecision({
|
|
373
|
+
renderedCopy: {
|
|
374
|
+
vellum: {
|
|
375
|
+
title: "Model headline",
|
|
376
|
+
body: "A description of what the run did.",
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
382
|
+
|
|
383
|
+
expect(item?.title).toBe("Payload headline");
|
|
384
|
+
expect(appendCalls[0]!.title).toBe("Payload headline");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("keeps a payload title that opens the summary", async () => {
|
|
388
|
+
// A correct topic headline is usually the opening noun phrase of the body,
|
|
389
|
+
// so an authored title that overlaps the summary still wins.
|
|
390
|
+
conversationRow = { conversationType: "background" };
|
|
391
|
+
const signal = makeSignal({
|
|
392
|
+
sourceEventName: "example.event",
|
|
393
|
+
contextPayload: { title: "Nightly Backup Failure" },
|
|
394
|
+
});
|
|
395
|
+
const decision = makeDecision({
|
|
396
|
+
renderedCopy: {
|
|
397
|
+
vellum: {
|
|
398
|
+
title: "Reminder plumbing check",
|
|
399
|
+
body: "Nightly backup failure on db-primary at 02:14. Retry scheduled.",
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
405
|
+
|
|
406
|
+
expect(item?.title).toBe("Nightly Backup Failure");
|
|
407
|
+
expect(appendCalls[0]!.summary).toBe(
|
|
408
|
+
"Nightly backup failure on db-primary at 02:14. Retry scheduled.",
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("keeps a payload title that matches the summary's first sentence", async () => {
|
|
413
|
+
conversationRow = { conversationType: "background" };
|
|
414
|
+
const signal = makeSignal({
|
|
415
|
+
sourceEventName: "example.event",
|
|
416
|
+
contextPayload: { title: "Test notifications reminder." },
|
|
417
|
+
});
|
|
418
|
+
const decision = makeDecision({
|
|
419
|
+
renderedCopy: {
|
|
420
|
+
vellum: {
|
|
421
|
+
title: "Model headline",
|
|
422
|
+
body: "Test notifications reminder. It fired on schedule.",
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
428
|
+
|
|
429
|
+
expect(item?.title).toBe("Test notifications reminder.");
|
|
430
|
+
expect(appendCalls[0]!.summary).toBe(
|
|
431
|
+
"Test notifications reminder. It fired on schedule.",
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("always writes a non-empty title even when the copy is unusable", async () => {
|
|
436
|
+
// `normalizeTitle` rejects prose-shaped candidates, so both authored
|
|
437
|
+
// titles drop out and the derivation carries the row.
|
|
438
|
+
conversationRow = { conversationType: "background" };
|
|
439
|
+
const signal = makeSignal({
|
|
440
|
+
sourceEventName: "example.event",
|
|
441
|
+
contextPayload: { title: "I need to name this notification somehow" },
|
|
442
|
+
});
|
|
443
|
+
const decision = makeDecision({
|
|
444
|
+
renderedCopy: {
|
|
445
|
+
vellum: {
|
|
446
|
+
title: "Let me summarize what happened",
|
|
447
|
+
body: "Disk usage crossed 90 percent. Cleanup ran.",
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
453
|
+
|
|
454
|
+
expect(item?.title).toBe("Disk usage crossed 90 percent.");
|
|
455
|
+
expect(appendCalls[0]!.title).toBeTruthy();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("derives a single-line plain title from a markdown-headed conversation seed", async () => {
|
|
459
|
+
// The summary prefers `conversationSeedMessage`, which carries structured
|
|
460
|
+
// markdown and hard line breaks. The derived title must not.
|
|
461
|
+
conversationRow = { conversationType: "background" };
|
|
462
|
+
const seed =
|
|
463
|
+
"## Nightly Backup Report\n\nThe backup job on db-primary failed at 02:14.";
|
|
464
|
+
const signal = makeSignal({ sourceEventName: "example.event" });
|
|
465
|
+
const decision = makeDecision({
|
|
466
|
+
renderedCopy: {
|
|
467
|
+
vellum: {
|
|
468
|
+
title: "",
|
|
469
|
+
body: "The backup job failed.",
|
|
470
|
+
conversationSeedMessage: seed,
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
476
|
+
|
|
477
|
+
expect(item?.title).toBe(
|
|
478
|
+
"Nightly Backup Report The backup job on db-primary failed at…",
|
|
479
|
+
);
|
|
480
|
+
expect(item?.summary).toBe(seed);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("strips tilde fences and indented headings, matching migration 138", async () => {
|
|
484
|
+
// Workspace migration 138 backfills titles with its own self-contained copy
|
|
485
|
+
// of these rules. A backfilled title and a freshly written one must match
|
|
486
|
+
// for the same summary, so both accept CommonMark's 3-space indent and both
|
|
487
|
+
// fence styles.
|
|
488
|
+
conversationRow = { conversationType: "background" };
|
|
489
|
+
const seed = " ## Indented heading\n\n~~~\ntilde fence\n~~~\nBody text.";
|
|
490
|
+
const signal = makeSignal({ sourceEventName: "example.event" });
|
|
491
|
+
const decision = makeDecision({
|
|
492
|
+
renderedCopy: {
|
|
493
|
+
vellum: {
|
|
494
|
+
title: "",
|
|
495
|
+
body: "Body text.",
|
|
496
|
+
conversationSeedMessage: seed,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
502
|
+
|
|
503
|
+
expect(item?.title).toBe("Indented heading tilde fence Body text.");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("derives a single-line plain title from a markdown-list conversation seed", async () => {
|
|
507
|
+
conversationRow = { conversationType: "background" };
|
|
508
|
+
const seed = "- Ran 12 checks\n- 3 failed\n\nSee the log for details.";
|
|
509
|
+
const signal = makeSignal({ sourceEventName: "example.event" });
|
|
510
|
+
const decision = makeDecision({
|
|
511
|
+
renderedCopy: {
|
|
512
|
+
vellum: {
|
|
513
|
+
title: "",
|
|
514
|
+
body: "Check run finished.",
|
|
515
|
+
conversationSeedMessage: seed,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
521
|
+
|
|
522
|
+
expect(item?.title).toBe("Ran 12 checks 3 failed See the log for details.");
|
|
523
|
+
expect(item?.summary).toBe(seed);
|
|
524
|
+
});
|
|
525
|
+
|
|
368
526
|
test("treats whitespace-only rendered copy and payload values as missing and returns null", async () => {
|
|
369
527
|
conversationRow = { conversationType: "background" };
|
|
370
528
|
const signal = makeSignal({
|
|
@@ -101,12 +101,15 @@ export class VellumAdapter implements ChannelAdapter {
|
|
|
101
101
|
this.broadcast({
|
|
102
102
|
type: "notification_intent",
|
|
103
103
|
deliveryId: payload.deliveryId,
|
|
104
|
+
correlationId: payload.correlationId,
|
|
104
105
|
sourceEventName: payload.sourceEventName,
|
|
105
106
|
title: payload.copy.title,
|
|
106
107
|
body: payload.copy.body,
|
|
107
108
|
deepLinkMetadata: payload.deepLinkTarget,
|
|
108
109
|
targetGuardianPrincipalId,
|
|
109
110
|
silent,
|
|
111
|
+
remotePushDispatched: payload.remotePushDispatched,
|
|
112
|
+
remotePushPlatforms: payload.remotePushPlatforms,
|
|
110
113
|
} as AssistantEvent);
|
|
111
114
|
|
|
112
115
|
log.info(
|