@vellumai/assistant 0.3.27 → 0.4.0
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 +81 -4
- package/Dockerfile +2 -2
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +9 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +21 -19
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +212 -36
- package/src/__tests__/notification-decision-fallback.test.ts +63 -3
- package/src/__tests__/notification-decision-strategy.test.ts +78 -0
- package/src/__tests__/notification-guardian-path.test.ts +15 -15
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +126 -59
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +358 -24
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +22 -16
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +33 -6
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +68 -326
- package/src/daemon/session-runtime-assembly.ts +119 -25
- package/src/daemon/session-tool-setup.ts +3 -2
- package/src/daemon/session.ts +4 -3
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +586 -0
- package/src/memory/channel-guardian-store.ts +2 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +56 -0
- package/src/notifications/copy-composer.ts +31 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +173 -0
- package/src/runtime/actor-trust-resolver.ts +221 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -71
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +717 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +20 -2
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +205 -529
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +53 -10
- package/src/tools/types.ts +13 -2
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -80,11 +80,12 @@ mock.module('../runtime/gateway-client.js', () => ({
|
|
|
80
80
|
},
|
|
81
81
|
}));
|
|
82
82
|
|
|
83
|
+
import { listCanonicalGuardianRequests } from '../memory/canonical-guardian-store.js';
|
|
83
84
|
import {
|
|
84
85
|
createBinding,
|
|
85
|
-
findPendingAccessRequestForRequester,
|
|
86
86
|
} from '../memory/channel-guardian-store.js';
|
|
87
87
|
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
88
|
+
import { notifyGuardianOfAccessRequest } from '../runtime/access-request-helper.js';
|
|
88
89
|
import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
|
|
89
90
|
|
|
90
91
|
initializeDb();
|
|
@@ -107,6 +108,8 @@ function resetState(): void {
|
|
|
107
108
|
db.run('DELETE FROM channel_inbound_events');
|
|
108
109
|
db.run('DELETE FROM conversations');
|
|
109
110
|
db.run('DELETE FROM notification_events');
|
|
111
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
112
|
+
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
110
113
|
emitSignalCalls.length = 0;
|
|
111
114
|
deliverReplyCalls.length = 0;
|
|
112
115
|
}
|
|
@@ -152,9 +155,10 @@ describe('non-member access request notification', () => {
|
|
|
152
155
|
expect(json.denied).toBe(true);
|
|
153
156
|
expect(json.reason).toBe('not_a_member');
|
|
154
157
|
|
|
155
|
-
// Rejection reply was delivered
|
|
158
|
+
// Rejection reply was delivered — always-notify behavior means the reply
|
|
159
|
+
// indicates the guardian will be notified, even without a same-channel binding.
|
|
156
160
|
expect(deliverReplyCalls.length).toBe(1);
|
|
157
|
-
expect((deliverReplyCalls[0].payload as Record<string, unknown>).text).toContain("
|
|
161
|
+
expect((deliverReplyCalls[0].payload as Record<string, unknown>).text).toContain("let them know");
|
|
158
162
|
});
|
|
159
163
|
|
|
160
164
|
test('guardian is notified when a non-member messages and a guardian binding exists', async () => {
|
|
@@ -185,18 +189,18 @@ describe('non-member access request notification', () => {
|
|
|
185
189
|
expect(payload.senderExternalUserId).toBe('user-unknown-456');
|
|
186
190
|
expect(payload.senderName).toBe('Alice Unknown');
|
|
187
191
|
|
|
188
|
-
//
|
|
189
|
-
const pending =
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
);
|
|
195
|
-
expect(pending).
|
|
196
|
-
expect(pending
|
|
197
|
-
expect(pending
|
|
198
|
-
expect(pending
|
|
199
|
-
expect(pending
|
|
192
|
+
// A canonical access request was created
|
|
193
|
+
const pending = listCanonicalGuardianRequests({
|
|
194
|
+
status: 'pending',
|
|
195
|
+
requesterExternalUserId: 'user-unknown-456',
|
|
196
|
+
sourceChannel: 'telegram',
|
|
197
|
+
kind: 'access_request',
|
|
198
|
+
});
|
|
199
|
+
expect(pending.length).toBe(1);
|
|
200
|
+
expect(pending[0].status).toBe('pending');
|
|
201
|
+
expect(pending[0].requesterExternalUserId).toBe('user-unknown-456');
|
|
202
|
+
expect(pending[0].guardianExternalUserId).toBe('guardian-user-789');
|
|
203
|
+
expect(pending[0].toolName).toBe('ingress_access_request');
|
|
200
204
|
});
|
|
201
205
|
|
|
202
206
|
test('no duplicate approval requests for repeated messages from same non-member', async () => {
|
|
@@ -224,18 +228,19 @@ describe('non-member access request notification', () => {
|
|
|
224
228
|
// Only one notification signal should be emitted (second is deduplicated)
|
|
225
229
|
expect(emitSignalCalls.length).toBe(1);
|
|
226
230
|
|
|
227
|
-
// Only one
|
|
228
|
-
const pending =
|
|
229
|
-
'
|
|
230
|
-
'
|
|
231
|
-
'
|
|
232
|
-
'
|
|
233
|
-
);
|
|
234
|
-
expect(pending).
|
|
231
|
+
// Only one canonical request should exist
|
|
232
|
+
const pending = listCanonicalGuardianRequests({
|
|
233
|
+
status: 'pending',
|
|
234
|
+
requesterExternalUserId: 'user-unknown-456',
|
|
235
|
+
sourceChannel: 'telegram',
|
|
236
|
+
kind: 'access_request',
|
|
237
|
+
});
|
|
238
|
+
expect(pending.length).toBe(1);
|
|
235
239
|
});
|
|
236
240
|
|
|
237
|
-
test('
|
|
238
|
-
// No guardian binding — should
|
|
241
|
+
test('access request is created and signal emitted even without same-channel guardian binding', async () => {
|
|
242
|
+
// No guardian binding on any channel — access request should still be
|
|
243
|
+
// created and notification signal emitted (null guardianExternalUserId).
|
|
239
244
|
const req = buildInboundRequest();
|
|
240
245
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
241
246
|
const json = await resp.json() as Record<string, unknown>;
|
|
@@ -243,20 +248,55 @@ describe('non-member access request notification', () => {
|
|
|
243
248
|
expect(json.denied).toBe(true);
|
|
244
249
|
expect(json.reason).toBe('not_a_member');
|
|
245
250
|
|
|
246
|
-
// Rejection reply was
|
|
251
|
+
// Rejection reply indicates guardian was notified
|
|
247
252
|
expect(deliverReplyCalls.length).toBe(1);
|
|
253
|
+
expect((deliverReplyCalls[0].payload as Record<string, unknown>).text).toContain("let them know");
|
|
248
254
|
|
|
249
|
-
//
|
|
250
|
-
expect(emitSignalCalls.length).toBe(
|
|
255
|
+
// Notification signal was emitted
|
|
256
|
+
expect(emitSignalCalls.length).toBe(1);
|
|
257
|
+
expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
|
|
251
258
|
|
|
252
|
-
//
|
|
253
|
-
const pending =
|
|
254
|
-
'
|
|
255
|
-
'
|
|
256
|
-
'
|
|
257
|
-
'
|
|
258
|
-
);
|
|
259
|
-
expect(pending).
|
|
259
|
+
// Canonical request was created with null guardianExternalUserId
|
|
260
|
+
const pending = listCanonicalGuardianRequests({
|
|
261
|
+
status: 'pending',
|
|
262
|
+
requesterExternalUserId: 'user-unknown-456',
|
|
263
|
+
sourceChannel: 'telegram',
|
|
264
|
+
kind: 'access_request',
|
|
265
|
+
});
|
|
266
|
+
expect(pending.length).toBe(1);
|
|
267
|
+
expect(pending[0].guardianExternalUserId).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('cross-channel fallback: SMS guardian binding resolves for Telegram access request', async () => {
|
|
271
|
+
// Only an SMS guardian binding exists — no Telegram binding
|
|
272
|
+
createBinding({
|
|
273
|
+
assistantId: 'self',
|
|
274
|
+
channel: 'sms',
|
|
275
|
+
guardianExternalUserId: 'guardian-sms-user',
|
|
276
|
+
guardianDeliveryChatId: 'guardian-sms-chat',
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const req = buildInboundRequest();
|
|
280
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
281
|
+
const json = await resp.json() as Record<string, unknown>;
|
|
282
|
+
|
|
283
|
+
expect(json.denied).toBe(true);
|
|
284
|
+
expect(json.reason).toBe('not_a_member');
|
|
285
|
+
|
|
286
|
+
// Notification signal emitted
|
|
287
|
+
expect(emitSignalCalls.length).toBe(1);
|
|
288
|
+
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
289
|
+
expect(payload.guardianBindingChannel).toBe('sms');
|
|
290
|
+
|
|
291
|
+
// Canonical request has the SMS guardian's external user ID
|
|
292
|
+
const pending = listCanonicalGuardianRequests({
|
|
293
|
+
status: 'pending',
|
|
294
|
+
requesterExternalUserId: 'user-unknown-456',
|
|
295
|
+
sourceChannel: 'telegram',
|
|
296
|
+
kind: 'access_request',
|
|
297
|
+
});
|
|
298
|
+
expect(pending.length).toBe(1);
|
|
299
|
+
expect(pending[0].guardianExternalUserId).toBe('guardian-sms-user');
|
|
260
300
|
});
|
|
261
301
|
|
|
262
302
|
test('no notification when senderExternalUserId is absent', async () => {
|
|
@@ -279,3 +319,139 @@ describe('non-member access request notification', () => {
|
|
|
279
319
|
expect(emitSignalCalls.length).toBe(0);
|
|
280
320
|
});
|
|
281
321
|
});
|
|
322
|
+
|
|
323
|
+
describe('access-request-helper unit tests', () => {
|
|
324
|
+
beforeEach(() => {
|
|
325
|
+
resetState();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('notifyGuardianOfAccessRequest returns no_sender_id when senderExternalUserId is absent', () => {
|
|
329
|
+
const result = notifyGuardianOfAccessRequest({
|
|
330
|
+
canonicalAssistantId: 'self',
|
|
331
|
+
sourceChannel: 'telegram',
|
|
332
|
+
externalChatId: 'chat-123',
|
|
333
|
+
senderExternalUserId: undefined,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(result.notified).toBe(false);
|
|
337
|
+
if (!result.notified) {
|
|
338
|
+
expect(result.reason).toBe('no_sender_id');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// No canonical request created
|
|
342
|
+
const pending = listCanonicalGuardianRequests({ status: 'pending', kind: 'access_request' });
|
|
343
|
+
expect(pending.length).toBe(0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('notifyGuardianOfAccessRequest creates request with null guardianExternalUserId when no binding exists', () => {
|
|
347
|
+
const result = notifyGuardianOfAccessRequest({
|
|
348
|
+
canonicalAssistantId: 'self',
|
|
349
|
+
sourceChannel: 'telegram',
|
|
350
|
+
externalChatId: 'chat-123',
|
|
351
|
+
senderExternalUserId: 'unknown-user',
|
|
352
|
+
senderName: 'Bob',
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(result.notified).toBe(true);
|
|
356
|
+
if (result.notified) {
|
|
357
|
+
expect(result.created).toBe(true);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const pending = listCanonicalGuardianRequests({
|
|
361
|
+
status: 'pending',
|
|
362
|
+
requesterExternalUserId: 'unknown-user',
|
|
363
|
+
kind: 'access_request',
|
|
364
|
+
});
|
|
365
|
+
expect(pending.length).toBe(1);
|
|
366
|
+
expect(pending[0].guardianExternalUserId).toBeNull();
|
|
367
|
+
|
|
368
|
+
// Signal was emitted
|
|
369
|
+
expect(emitSignalCalls.length).toBe(1);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('notifyGuardianOfAccessRequest uses cross-channel binding when source-channel binding is missing', () => {
|
|
373
|
+
// Only SMS binding exists
|
|
374
|
+
createBinding({
|
|
375
|
+
assistantId: 'self',
|
|
376
|
+
channel: 'sms',
|
|
377
|
+
guardianExternalUserId: 'guardian-sms',
|
|
378
|
+
guardianDeliveryChatId: 'sms-chat',
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const result = notifyGuardianOfAccessRequest({
|
|
382
|
+
canonicalAssistantId: 'self',
|
|
383
|
+
sourceChannel: 'telegram',
|
|
384
|
+
externalChatId: 'tg-chat',
|
|
385
|
+
senderExternalUserId: 'unknown-tg-user',
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(result.notified).toBe(true);
|
|
389
|
+
|
|
390
|
+
const pending = listCanonicalGuardianRequests({
|
|
391
|
+
status: 'pending',
|
|
392
|
+
requesterExternalUserId: 'unknown-tg-user',
|
|
393
|
+
kind: 'access_request',
|
|
394
|
+
});
|
|
395
|
+
expect(pending.length).toBe(1);
|
|
396
|
+
expect(pending[0].guardianExternalUserId).toBe('guardian-sms');
|
|
397
|
+
|
|
398
|
+
// Signal payload includes fallback channel
|
|
399
|
+
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
400
|
+
expect(payload.guardianBindingChannel).toBe('sms');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('notifyGuardianOfAccessRequest prefers source-channel binding over cross-channel fallback', () => {
|
|
404
|
+
// Both Telegram and SMS bindings exist
|
|
405
|
+
createBinding({
|
|
406
|
+
assistantId: 'self',
|
|
407
|
+
channel: 'telegram',
|
|
408
|
+
guardianExternalUserId: 'guardian-tg',
|
|
409
|
+
guardianDeliveryChatId: 'tg-chat',
|
|
410
|
+
});
|
|
411
|
+
createBinding({
|
|
412
|
+
assistantId: 'self',
|
|
413
|
+
channel: 'sms',
|
|
414
|
+
guardianExternalUserId: 'guardian-sms',
|
|
415
|
+
guardianDeliveryChatId: 'sms-chat',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const result = notifyGuardianOfAccessRequest({
|
|
419
|
+
canonicalAssistantId: 'self',
|
|
420
|
+
sourceChannel: 'telegram',
|
|
421
|
+
externalChatId: 'chat-123',
|
|
422
|
+
senderExternalUserId: 'unknown-user',
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(result.notified).toBe(true);
|
|
426
|
+
|
|
427
|
+
const pending = listCanonicalGuardianRequests({
|
|
428
|
+
status: 'pending',
|
|
429
|
+
requesterExternalUserId: 'unknown-user',
|
|
430
|
+
kind: 'access_request',
|
|
431
|
+
});
|
|
432
|
+
expect(pending.length).toBe(1);
|
|
433
|
+
// Should use the Telegram binding, not SMS fallback
|
|
434
|
+
expect(pending[0].guardianExternalUserId).toBe('guardian-tg');
|
|
435
|
+
|
|
436
|
+
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
437
|
+
expect(payload.guardianBindingChannel).toBe('telegram');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('notifyGuardianOfAccessRequest includes requestCode in contextPayload', () => {
|
|
441
|
+
const result = notifyGuardianOfAccessRequest({
|
|
442
|
+
canonicalAssistantId: 'self',
|
|
443
|
+
sourceChannel: 'telegram',
|
|
444
|
+
externalChatId: 'chat-123',
|
|
445
|
+
senderExternalUserId: 'unknown-user',
|
|
446
|
+
senderName: 'Test User',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(result.notified).toBe(true);
|
|
450
|
+
expect(emitSignalCalls.length).toBe(1);
|
|
451
|
+
|
|
452
|
+
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
453
|
+
expect(payload.requestCode).toBeDefined();
|
|
454
|
+
expect(typeof payload.requestCode).toBe('string');
|
|
455
|
+
expect((payload.requestCode as string).length).toBe(6);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* decision-model call is unavailable.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, expect, mock, test } from 'bun:test';
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
9
9
|
|
|
10
10
|
mock.module('../channels/config.js', () => ({
|
|
11
11
|
getDeliverableChannels: () => ['vellum', 'telegram', 'sms'],
|
|
@@ -13,6 +13,8 @@ mock.module('../channels/config.js', () => ({
|
|
|
13
13
|
|
|
14
14
|
mock.module('../config/loader.js', () => ({
|
|
15
15
|
getConfig: () => ({
|
|
16
|
+
ui: {},
|
|
17
|
+
|
|
16
18
|
notifications: {
|
|
17
19
|
decisionModelIntent: 'latency-optimized',
|
|
18
20
|
},
|
|
@@ -32,13 +34,16 @@ mock.module('../notifications/thread-candidates.js', () => ({
|
|
|
32
34
|
serializeCandidatesForPrompt: () => undefined,
|
|
33
35
|
}));
|
|
34
36
|
|
|
37
|
+
let configuredProvider: { sendMessage: () => Promise<unknown> } | null = null;
|
|
38
|
+
let extractedToolUse: unknown = null;
|
|
39
|
+
|
|
35
40
|
mock.module('../providers/provider-send-message.js', () => ({
|
|
36
|
-
getConfiguredProvider: () =>
|
|
41
|
+
getConfiguredProvider: () => configuredProvider,
|
|
37
42
|
createTimeout: () => ({
|
|
38
43
|
signal: new AbortController().signal,
|
|
39
44
|
cleanup: () => {},
|
|
40
45
|
}),
|
|
41
|
-
extractToolUse: () =>
|
|
46
|
+
extractToolUse: () => extractedToolUse,
|
|
42
47
|
userMessage: (text: string) => ({ role: 'user', content: text }),
|
|
43
48
|
}));
|
|
44
49
|
|
|
@@ -75,6 +80,11 @@ function makeSignal(overrides?: Partial<NotificationSignal>): NotificationSignal
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
describe('notification decision fallback copy', () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
configuredProvider = null;
|
|
85
|
+
extractedToolUse = null;
|
|
86
|
+
});
|
|
87
|
+
|
|
78
88
|
test('uses human-friendly template copy for guardian.question', async () => {
|
|
79
89
|
const signal = makeSignal();
|
|
80
90
|
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
@@ -85,4 +95,54 @@ describe('notification decision fallback copy', () => {
|
|
|
85
95
|
expect(decision.renderedCopy.vellum?.title).not.toBe('guardian.question');
|
|
86
96
|
expect(decision.renderedCopy.vellum?.body).not.toContain('Action required: guardian.question');
|
|
87
97
|
});
|
|
98
|
+
|
|
99
|
+
test('enforces request-code instructions for guardian.question when requestCode exists', async () => {
|
|
100
|
+
const signal = makeSignal({
|
|
101
|
+
contextPayload: {
|
|
102
|
+
questionText: 'What is the gate code?',
|
|
103
|
+
requestCode: 'A1B2C3',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
107
|
+
|
|
108
|
+
expect(decision.fallbackUsed).toBe(true);
|
|
109
|
+
expect(decision.renderedCopy.vellum?.body).toContain('A1B2C3');
|
|
110
|
+
expect(decision.renderedCopy.vellum?.body).toContain('approve');
|
|
111
|
+
expect(decision.renderedCopy.vellum?.body).toContain('reject');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('enforcement appends explicit approve/reject instructions when LLM copy only mentions request code', async () => {
|
|
115
|
+
configuredProvider = {
|
|
116
|
+
sendMessage: async () => ({ content: [] }),
|
|
117
|
+
};
|
|
118
|
+
extractedToolUse = {
|
|
119
|
+
name: 'record_notification_decision',
|
|
120
|
+
input: {
|
|
121
|
+
shouldNotify: true,
|
|
122
|
+
selectedChannels: ['vellum'],
|
|
123
|
+
reasoningSummary: 'LLM decision',
|
|
124
|
+
renderedCopy: {
|
|
125
|
+
vellum: {
|
|
126
|
+
title: 'Guardian Question',
|
|
127
|
+
body: 'Use reference code A1B2C3 for this request.',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
dedupeKey: 'guardian-question-test',
|
|
131
|
+
confidence: 0.9,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const signal = makeSignal({
|
|
136
|
+
contextPayload: {
|
|
137
|
+
questionText: 'What is the gate code?',
|
|
138
|
+
requestCode: 'A1B2C3',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
143
|
+
|
|
144
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
145
|
+
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 approve"');
|
|
146
|
+
expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 reject"');
|
|
147
|
+
});
|
|
88
148
|
});
|
|
@@ -55,6 +55,23 @@ describe('notification decision strategy', () => {
|
|
|
55
55
|
expect(copy.vellum!.body).toContain('What is the gate code?');
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
test('guardian.question template includes request-code instructions when present', () => {
|
|
59
|
+
const signal = makeSignal({
|
|
60
|
+
sourceEventName: 'guardian.question',
|
|
61
|
+
contextPayload: {
|
|
62
|
+
questionText: 'What is the gate code?',
|
|
63
|
+
requestCode: 'A1B2C3',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
68
|
+
expect(copy.vellum).toBeDefined();
|
|
69
|
+
expect(copy.vellum!.body).toContain('A1B2C3');
|
|
70
|
+
expect(copy.vellum!.body).toContain('approve');
|
|
71
|
+
expect(copy.vellum!.body).toContain('reject');
|
|
72
|
+
expect(copy.telegram!.deliveryText).toContain('A1B2C3');
|
|
73
|
+
});
|
|
74
|
+
|
|
58
75
|
test('reminder.fired template uses message from payload', () => {
|
|
59
76
|
const signal = makeSignal({
|
|
60
77
|
sourceEventName: 'reminder.fired',
|
|
@@ -134,6 +151,67 @@ describe('notification decision strategy', () => {
|
|
|
134
151
|
expect(copy.vellum!.deliveryText).toBeUndefined();
|
|
135
152
|
});
|
|
136
153
|
|
|
154
|
+
test('ingress.access_request template includes requester identifier', () => {
|
|
155
|
+
const signal = makeSignal({
|
|
156
|
+
sourceEventName: 'ingress.access_request',
|
|
157
|
+
contextPayload: {
|
|
158
|
+
senderIdentifier: 'Alice',
|
|
159
|
+
requestCode: 'A1B2C3',
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
164
|
+
expect(copy.vellum).toBeDefined();
|
|
165
|
+
expect(copy.vellum!.title).toBe('Access Request');
|
|
166
|
+
expect(copy.vellum!.body).toContain('Alice');
|
|
167
|
+
expect(copy.vellum!.body).toContain('requesting access');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('ingress.access_request template includes request code instruction when present', () => {
|
|
171
|
+
const signal = makeSignal({
|
|
172
|
+
sourceEventName: 'ingress.access_request',
|
|
173
|
+
contextPayload: {
|
|
174
|
+
senderIdentifier: 'Bob',
|
|
175
|
+
requestCode: 'D4E5F6',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
180
|
+
expect(copy.vellum).toBeDefined();
|
|
181
|
+
expect(copy.vellum!.body).toContain('D4E5F6');
|
|
182
|
+
expect(copy.vellum!.body).toContain('approve');
|
|
183
|
+
expect(copy.vellum!.body).toContain('reject');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('ingress.access_request template includes invite flow instruction', () => {
|
|
187
|
+
const signal = makeSignal({
|
|
188
|
+
sourceEventName: 'ingress.access_request',
|
|
189
|
+
contextPayload: {
|
|
190
|
+
senderIdentifier: 'Charlie',
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
195
|
+
expect(copy.vellum).toBeDefined();
|
|
196
|
+
expect(copy.vellum!.body).toContain('open invite flow');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('ingress.access_request Telegram deliveryText is concise', () => {
|
|
200
|
+
const signal = makeSignal({
|
|
201
|
+
sourceEventName: 'ingress.access_request',
|
|
202
|
+
contextPayload: {
|
|
203
|
+
senderIdentifier: 'Dave',
|
|
204
|
+
requestCode: 'ABC123',
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const copy = composeFallbackCopy(signal, ['telegram']);
|
|
209
|
+
expect(copy.telegram).toBeDefined();
|
|
210
|
+
expect(copy.telegram!.deliveryText).toBeDefined();
|
|
211
|
+
expect(typeof copy.telegram!.deliveryText).toBe('string');
|
|
212
|
+
expect(copy.telegram!.deliveryText!.length).toBeGreaterThan(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
137
215
|
test('empty payload falls back to default text in template', () => {
|
|
138
216
|
const signal = makeSignal({
|
|
139
217
|
sourceEventName: 'guardian.question',
|
|
@@ -47,6 +47,8 @@ mock.module('../memory/channel-guardian-store.js', () => ({
|
|
|
47
47
|
|
|
48
48
|
mock.module('../config/loader.js', () => ({
|
|
49
49
|
getConfig: () => ({
|
|
50
|
+
ui: {},
|
|
51
|
+
|
|
50
52
|
calls: {
|
|
51
53
|
userConsultTimeoutSeconds: 120,
|
|
52
54
|
},
|
|
@@ -108,6 +110,8 @@ function ensureConversation(id: string): void {
|
|
|
108
110
|
|
|
109
111
|
function resetTables(): void {
|
|
110
112
|
const db = getDb();
|
|
113
|
+
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
114
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
111
115
|
db.run('DELETE FROM guardian_action_deliveries');
|
|
112
116
|
db.run('DELETE FROM guardian_action_requests');
|
|
113
117
|
db.run('DELETE FROM call_pending_questions');
|
|
@@ -268,16 +272,15 @@ describe('ASK_GUARDIAN canonical notification path', () => {
|
|
|
268
272
|
|
|
269
273
|
const db = getDb();
|
|
270
274
|
const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
271
|
-
const request = raw.query('SELECT * FROM
|
|
275
|
+
const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
|
|
272
276
|
| { id: string }
|
|
273
277
|
| undefined;
|
|
274
278
|
const deliveries = raw.query(
|
|
275
|
-
'SELECT destination_channel, destination_conversation_id, destination_chat_id,
|
|
279
|
+
'SELECT destination_channel, destination_conversation_id, destination_chat_id, status FROM canonical_guardian_deliveries WHERE request_id = ? ORDER BY destination_channel ASC',
|
|
276
280
|
).all(request!.id) as Array<{
|
|
277
281
|
destination_channel: string;
|
|
278
282
|
destination_conversation_id: string | null;
|
|
279
283
|
destination_chat_id: string | null;
|
|
280
|
-
destination_external_user_id: string | null;
|
|
281
284
|
status: string;
|
|
282
285
|
}>;
|
|
283
286
|
|
|
@@ -286,7 +289,6 @@ describe('ASK_GUARDIAN canonical notification path', () => {
|
|
|
286
289
|
const vellum = deliveries.find((d) => d.destination_channel === 'vellum');
|
|
287
290
|
expect(telegram).toBeDefined();
|
|
288
291
|
expect(telegram!.destination_chat_id).toBe('tg-chat-abc');
|
|
289
|
-
expect(telegram!.destination_external_user_id).toBe('tg-user-xyz');
|
|
290
292
|
expect(telegram!.status).toBe('sent');
|
|
291
293
|
expect(vellum).toBeDefined();
|
|
292
294
|
expect(vellum!.destination_conversation_id).toBe('conv-guardian-vellum');
|
|
@@ -322,16 +324,15 @@ describe('ASK_GUARDIAN canonical notification path', () => {
|
|
|
322
324
|
|
|
323
325
|
const db = getDb();
|
|
324
326
|
const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
325
|
-
const request = raw.query('SELECT * FROM
|
|
327
|
+
const request = raw.query('SELECT * FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
|
|
326
328
|
| { id: string }
|
|
327
329
|
| undefined;
|
|
328
330
|
const vellumDelivery = raw.query(
|
|
329
|
-
'SELECT status
|
|
330
|
-
).get(request!.id, 'vellum') as { status: string
|
|
331
|
+
'SELECT status FROM canonical_guardian_deliveries WHERE request_id = ? AND destination_channel = ?',
|
|
332
|
+
).get(request!.id, 'vellum') as { status: string } | undefined;
|
|
331
333
|
|
|
332
334
|
expect(vellumDelivery).toBeDefined();
|
|
333
335
|
expect(vellumDelivery!.status).toBe('failed');
|
|
334
|
-
expect(vellumDelivery!.last_error).toContain('No vellum delivery result');
|
|
335
336
|
});
|
|
336
337
|
|
|
337
338
|
test('context payload includes callSessionId and activeGuardianRequestCount for candidate-affinity', async () => {
|
|
@@ -426,11 +427,11 @@ describe('ASK_GUARDIAN canonical notification path', () => {
|
|
|
426
427
|
pendingQuestion: pq2,
|
|
427
428
|
});
|
|
428
429
|
|
|
429
|
-
// Verify: two distinct
|
|
430
|
+
// Verify: two distinct canonical_guardian_requests exist
|
|
430
431
|
const db = getDb();
|
|
431
432
|
const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
432
433
|
const requests = raw.query(
|
|
433
|
-
'SELECT id, question_text FROM
|
|
434
|
+
'SELECT id, question_text FROM canonical_guardian_requests WHERE call_session_id = ? ORDER BY created_at ASC',
|
|
434
435
|
).all(session.id) as Array<{ id: string; question_text: string }>;
|
|
435
436
|
expect(requests).toHaveLength(2);
|
|
436
437
|
expect(requests[0].question_text).toBe('Can they enter through the side gate?');
|
|
@@ -438,7 +439,7 @@ describe('ASK_GUARDIAN canonical notification path', () => {
|
|
|
438
439
|
|
|
439
440
|
// Verify: each request has its own delivery row pointing to the shared conversation
|
|
440
441
|
const deliveries = raw.query(
|
|
441
|
-
'SELECT request_id, destination_conversation_id, status FROM
|
|
442
|
+
'SELECT request_id, destination_conversation_id, status FROM canonical_guardian_deliveries WHERE destination_conversation_id = ? ORDER BY created_at ASC',
|
|
442
443
|
).all(sharedConvId) as Array<{ request_id: string; destination_conversation_id: string; status: string }>;
|
|
443
444
|
expect(deliveries).toHaveLength(2);
|
|
444
445
|
expect(deliveries[0].request_id).toBe(requests[0].id);
|
|
@@ -478,16 +479,15 @@ describe('ASK_GUARDIAN canonical notification path', () => {
|
|
|
478
479
|
// The dispatch should still create a failed fallback delivery row
|
|
479
480
|
const db = getDb();
|
|
480
481
|
const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
481
|
-
const request = raw.query('SELECT id FROM
|
|
482
|
+
const request = raw.query('SELECT id FROM canonical_guardian_requests WHERE call_session_id = ?').get(session.id) as
|
|
482
483
|
| { id: string }
|
|
483
484
|
| undefined;
|
|
484
485
|
expect(request).toBeDefined();
|
|
485
486
|
|
|
486
487
|
const delivery = raw.query(
|
|
487
|
-
'SELECT status
|
|
488
|
-
).get(request!.id, 'vellum') as { status: string
|
|
488
|
+
'SELECT status FROM canonical_guardian_deliveries WHERE request_id = ? AND destination_channel = ?',
|
|
489
|
+
).get(request!.id, 'vellum') as { status: string } | undefined;
|
|
489
490
|
expect(delivery).toBeDefined();
|
|
490
491
|
expect(delivery!.status).toBe('failed');
|
|
491
|
-
expect(delivery!.last_error).toContain('No vellum delivery result');
|
|
492
492
|
});
|
|
493
493
|
});
|
|
@@ -39,7 +39,9 @@ mock.module('../util/logger.js', () => ({
|
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
41
|
mock.module('../config/loader.js', () => ({
|
|
42
|
-
getConfig: () => ({
|
|
42
|
+
getConfig: () => ({
|
|
43
|
+
ui: {},
|
|
44
|
+
}),
|
|
43
45
|
loadConfig: () => ({ ingress: { publicBaseUrl: 'https://test.example.com' } }),
|
|
44
46
|
loadRawConfig: () => ({}),
|
|
45
47
|
saveRawConfig: () => {},
|
|
@@ -151,7 +151,7 @@ describe('starter task playbook integration with buildSystemPrompt', () => {
|
|
|
151
151
|
expect(result).not.toContain('## Starter Task Playbooks');
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test('starter task playbook
|
|
154
|
+
test('starter task playbook and channel awareness both present during onboarding', () => {
|
|
155
155
|
writeFileSync(join(TEST_DIR, 'IDENTITY.md'), 'I am Vellum.');
|
|
156
156
|
writeFileSync(join(TEST_DIR, 'BOOTSTRAP.md'), '# First run');
|
|
157
157
|
const result = buildSystemPrompt();
|
|
@@ -159,7 +159,6 @@ describe('starter task playbook integration with buildSystemPrompt', () => {
|
|
|
159
159
|
const channelIdx = result.indexOf('## Channel Awareness & Trust Gating');
|
|
160
160
|
expect(starterIdx).toBeGreaterThan(-1);
|
|
161
161
|
expect(channelIdx).toBeGreaterThan(-1);
|
|
162
|
-
expect(starterIdx).toBeLessThan(channelIdx);
|
|
163
162
|
});
|
|
164
163
|
|
|
165
164
|
test('all three kickoff intents present in full system prompt during onboarding', () => {
|
|
@@ -171,9 +170,10 @@ describe('starter task playbook integration with buildSystemPrompt', () => {
|
|
|
171
170
|
expect(result).toContain('[STARTER_TASK:research_to_ui]');
|
|
172
171
|
});
|
|
173
172
|
|
|
174
|
-
test('system prompt does not contain invalid config_update surface type', () => {
|
|
173
|
+
test('system prompt does not contain invalid config_update surface type (bare)', () => {
|
|
175
174
|
writeFileSync(join(TEST_DIR, 'IDENTITY.md'), 'I am Vellum.');
|
|
176
175
|
const result = buildSystemPrompt();
|
|
177
|
-
|
|
176
|
+
// voice_config_update is a valid tool name; only bare 'config_update' surface type is invalid
|
|
177
|
+
expect(result).not.toContain('surface_type: "config_update"');
|
|
178
178
|
});
|
|
179
179
|
});
|