@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.
- package/ARCHITECTURE.md +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- 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",
|