@vellumai/assistant 0.8.2 → 0.8.3

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.
Files changed (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Tests for the deterministic pre-send checks.
3
+ *
4
+ * Focus: the rendered-copy quality check that suppresses notifications
5
+ * with empty bodies or bodies that leak the raw source event name
6
+ * (the `buildGenericCopy` fallback path).
7
+ */
8
+
9
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ mock.module("../../util/logger.js", () => ({
12
+ getLogger: () =>
13
+ new Proxy({} as Record<string, unknown>, {
14
+ get: () => () => {},
15
+ }),
16
+ truncateForLog: (value: string) => value,
17
+ }));
18
+
19
+ import { getDb } from "../../memory/db-connection.js";
20
+ import { initializeDb } from "../../memory/db-init.js";
21
+ import { notificationEvents } from "../../memory/schema.js";
22
+ import {
23
+ type DeterministicCheckContext,
24
+ runDeterministicChecks,
25
+ } from "../deterministic-checks.js";
26
+ import type { NotificationSignal } from "../signal.js";
27
+ import type { NotificationDecision } from "../types.js";
28
+
29
+ initializeDb();
30
+
31
+ beforeEach(() => {
32
+ getDb().delete(notificationEvents).run();
33
+ });
34
+
35
+ function makeSignal(
36
+ overrides?: Partial<NotificationSignal>,
37
+ ): NotificationSignal {
38
+ return {
39
+ signalId: `sig-${crypto.randomUUID()}`,
40
+ createdAt: Date.now(),
41
+ sourceChannel: "scheduler",
42
+ sourceContextId: "ctx-1",
43
+ sourceEventName: "schedule.notify",
44
+ contextPayload: {},
45
+ attentionHints: {
46
+ requiresAction: false,
47
+ urgency: "low",
48
+ isAsyncBackground: false,
49
+ visibleInSourceNow: false,
50
+ },
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function makeDecision(
56
+ overrides?: Partial<NotificationDecision>,
57
+ ): NotificationDecision {
58
+ return {
59
+ shouldNotify: true,
60
+ selectedChannels: ["vellum"],
61
+ reasoningSummary: "test",
62
+ renderedCopy: {
63
+ vellum: { title: "Reminder", body: "Time to drink water" },
64
+ },
65
+ dedupeKey: `dk-${crypto.randomUUID()}`,
66
+ confidence: 0.9,
67
+ fallbackUsed: false,
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ const context: DeterministicCheckContext = {
73
+ connectedChannels: ["vellum"],
74
+ };
75
+
76
+ describe("checkRenderedCopyQuality (via runDeterministicChecks)", () => {
77
+ test("passes when body is real non-empty text", async () => {
78
+ const result = await runDeterministicChecks(
79
+ makeSignal(),
80
+ makeDecision(),
81
+ context,
82
+ );
83
+ expect(result.passed).toBe(true);
84
+ });
85
+
86
+ test("fails when body is empty", async () => {
87
+ const decision = makeDecision({
88
+ renderedCopy: {
89
+ vellum: { title: "Reminder", body: "" },
90
+ },
91
+ });
92
+ const result = await runDeterministicChecks(
93
+ makeSignal(),
94
+ decision,
95
+ context,
96
+ );
97
+ expect(result.passed).toBe(false);
98
+ expect(result.reason).toContain("empty");
99
+ });
100
+
101
+ test("fails when body is whitespace only", async () => {
102
+ const decision = makeDecision({
103
+ renderedCopy: {
104
+ vellum: { title: "Reminder", body: " \n " },
105
+ },
106
+ });
107
+ const result = await runDeterministicChecks(
108
+ makeSignal(),
109
+ decision,
110
+ context,
111
+ );
112
+ expect(result.passed).toBe(false);
113
+ expect(result.reason).toContain("empty");
114
+ });
115
+
116
+ test("fails when body is the raw source event name", async () => {
117
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
118
+ const decision = makeDecision({
119
+ renderedCopy: {
120
+ vellum: { title: "Reminder", body: "user.send_notification" },
121
+ },
122
+ });
123
+ const result = await runDeterministicChecks(signal, decision, context);
124
+ expect(result.passed).toBe(false);
125
+ expect(result.reason).toContain("fallback leak");
126
+ });
127
+
128
+ test("fails when body matches the normalized source event name", async () => {
129
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
130
+ const decision = makeDecision({
131
+ renderedCopy: {
132
+ vellum: { title: "Reminder", body: "user send notification" },
133
+ },
134
+ });
135
+ const result = await runDeterministicChecks(signal, decision, context);
136
+ expect(result.passed).toBe(false);
137
+ expect(result.reason).toContain("fallback leak");
138
+ });
139
+
140
+ test("passes when channel was appended post-decision (urgency-forced vellum prepend)", async () => {
141
+ // Regression: emit-signal.ts prepends `vellum` to selectedChannels for
142
+ // high/critical urgency without populating renderedCopy.vellum. The
143
+ // broadcaster's composeFallbackCopy rescue handles those channels at
144
+ // delivery time, so the deterministic check must not fail-closed here.
145
+ const signal = makeSignal({
146
+ attentionHints: {
147
+ requiresAction: false,
148
+ urgency: "high",
149
+ isAsyncBackground: false,
150
+ visibleInSourceNow: false,
151
+ },
152
+ });
153
+ const decision = makeDecision({
154
+ selectedChannels: ["vellum", "telegram"],
155
+ renderedCopy: {
156
+ telegram: { title: "Reminder", body: "Time to drink water" },
157
+ },
158
+ });
159
+ const result = await runDeterministicChecks(signal, decision, {
160
+ connectedChannels: ["vellum", "telegram"],
161
+ });
162
+ expect(result.passed).toBe(true);
163
+ });
164
+
165
+ test("passes when enforceRoutingIntent expanded channels post-decision", async () => {
166
+ // Regression: enforceRoutingIntent can expand selectedChannels to
167
+ // all_channels / multi_channel without populating renderedCopy for the
168
+ // added channels. Broadcaster fallback covers them — check must allow.
169
+ const decision = makeDecision({
170
+ selectedChannels: ["vellum", "telegram", "slack"],
171
+ renderedCopy: {
172
+ vellum: { title: "Reminder", body: "Time to drink water" },
173
+ },
174
+ });
175
+ const result = await runDeterministicChecks(makeSignal(), decision, {
176
+ connectedChannels: ["vellum", "telegram", "slack"],
177
+ });
178
+ expect(result.passed).toBe(true);
179
+ });
180
+
181
+ test("still validates body quality for channels with rendered copy", async () => {
182
+ // Even when some channels lack copy (broadcaster fallback territory),
183
+ // channels that DO have copy must still pass the empty/event-name checks.
184
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
185
+ const decision = makeDecision({
186
+ selectedChannels: ["vellum", "telegram"],
187
+ renderedCopy: {
188
+ telegram: { title: "Reminder", body: "user.send_notification" },
189
+ },
190
+ });
191
+ const result = await runDeterministicChecks(signal, decision, {
192
+ connectedChannels: ["vellum", "telegram"],
193
+ });
194
+ expect(result.passed).toBe(false);
195
+ expect(result.reason).toContain("fallback leak");
196
+ });
197
+
198
+ test("fails when no selected channel has copy and fallback body is empty", async () => {
199
+ // Silent-no-delivery guard: if every selected channel is missing from
200
+ // renderedCopy AND the broadcaster's composeFallbackCopy can't produce
201
+ // a usable body (no template for sourceEventName → buildGenericCopy
202
+ // returns body=""), the gate must fail-closed rather than letting
203
+ // dispatchDecision report 0/N sent.
204
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
205
+ const decision = makeDecision({
206
+ selectedChannels: ["vellum"],
207
+ renderedCopy: {},
208
+ });
209
+ const result = await runDeterministicChecks(signal, decision, context);
210
+ expect(result.passed).toBe(false);
211
+ expect(result.reason).toContain("fallback");
212
+ });
213
+
214
+ test("passes when no selected channel has copy but fallback yields a usable body", async () => {
215
+ // schedule.notify has a copy-composer template that produces a usable
216
+ // body even with empty payload — the broadcaster's fallback path will
217
+ // deliver, so the deterministic gate must allow it through.
218
+ const signal = makeSignal({ sourceEventName: "schedule.notify" });
219
+ const decision = makeDecision({
220
+ selectedChannels: ["vellum"],
221
+ renderedCopy: {},
222
+ });
223
+ const result = await runDeterministicChecks(signal, decision, context);
224
+ expect(result.passed).toBe(true);
225
+ });
226
+
227
+ test("passes when shouldNotify is false regardless of copy contents", async () => {
228
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
229
+ const decision = makeDecision({
230
+ shouldNotify: false,
231
+ // Empty body + event-name body would both fail the copy check if
232
+ // shouldNotify were true. Short-circuit must skip the check.
233
+ renderedCopy: {
234
+ vellum: { title: "", body: "" },
235
+ },
236
+ });
237
+ const result = await runDeterministicChecks(signal, decision, context);
238
+ expect(result.passed).toBe(true);
239
+ });
240
+
241
+ test("passes assistant_tool pass-through even when body matches normalized event name", async () => {
242
+ // The pass-through path produces verbatim user-supplied body text.
243
+ // A coincidental match with the source event name is the user's
244
+ // intent, not a fallback leak — the check must not suppress it.
245
+ const signal = makeSignal({
246
+ sourceChannel: "assistant_tool",
247
+ sourceEventName: "assistant.share",
248
+ });
249
+ const decision = makeDecision({
250
+ reasoningSummary: "assistant_tool pass-through",
251
+ renderedCopy: {
252
+ vellum: { title: "Assistant share", body: "assistant share" },
253
+ },
254
+ });
255
+ const result = await runDeterministicChecks(signal, decision, context);
256
+ expect(result.passed).toBe(true);
257
+ });
258
+
259
+ test("fails assistant_tool pass-through with empty body (empty-body branch still fires)", async () => {
260
+ const signal = makeSignal({ sourceChannel: "assistant_tool" });
261
+ const decision = makeDecision({
262
+ reasoningSummary: "assistant_tool pass-through",
263
+ renderedCopy: {
264
+ vellum: { title: "Reminder", body: "" },
265
+ },
266
+ });
267
+ const result = await runDeterministicChecks(signal, decision, context);
268
+ expect(result.passed).toBe(false);
269
+ expect(result.reason).toContain("empty");
270
+ });
271
+
272
+ test("still fails non-pass-through decision when body matches event name", async () => {
273
+ // Regression guard: the pass-through short-circuit must not weaken
274
+ // the check for LLM/fallback paths.
275
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
276
+ const decision = makeDecision({
277
+ reasoningSummary: "llm classification",
278
+ renderedCopy: {
279
+ vellum: { title: "Reminder", body: "user.send_notification" },
280
+ },
281
+ });
282
+ const result = await runDeterministicChecks(signal, decision, context);
283
+ expect(result.passed).toBe(false);
284
+ expect(result.reason).toContain("fallback leak");
285
+ });
286
+ });
@@ -144,6 +144,7 @@ describe("emitNotificationSignal home-feed wire-up", () => {
144
144
  sourceEventName: "schedule.notify",
145
145
  sourceChannel: "scheduler",
146
146
  sourceContextId: "conv-source-1",
147
+ contextPayload: { title: "Background job done" },
147
148
  attentionHints: {
148
149
  requiresAction: false,
149
150
  urgency: "medium",