@vellumai/assistant 0.4.3 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +40 -3
- package/README.md +43 -35
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -87
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +4 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -4
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +131 -8
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +62 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +841 -39
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +10 -2
- package/src/approvals/guardian-request-resolvers.ts +128 -9
- package/src/calls/call-constants.ts +21 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-domain.ts +28 -7
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +424 -12
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
- package/src/config/calls-schema.ts +24 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +5 -55
- package/src/daemon/handlers/config-inbox.ts +9 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/sessions.ts +48 -3
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/integrations.ts +1 -99
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +14 -1
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +22 -11
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +3 -0
- package/src/daemon/session-surfaces.ts +3 -2
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +16 -0
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +39 -1
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +16 -8
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +71 -1
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +0 -3
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +65 -12
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/invite-redemption-service.ts +8 -0
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +140 -52
- package/src/runtime/routes/events-routes.ts +20 -5
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-message-handler.ts +143 -2
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/tool-approval-handler.ts +242 -18
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -196,8 +196,8 @@ describe('Invariant 2: no generic plaintext secret read API', () => {
|
|
|
196
196
|
'daemon/handlers.ts', // Vercel API token + integration OAuth
|
|
197
197
|
'daemon/handlers/config-integrations.ts', // Vercel API token + Twitter integration OAuth
|
|
198
198
|
'daemon/handlers/config-telegram.ts', // Telegram bot token management
|
|
199
|
-
'daemon/handlers/config-twilio.ts', // Twilio credential management
|
|
200
199
|
'daemon/handlers/config-ingress.ts', // Ingress config (reads Twilio credentials for webhook sync)
|
|
200
|
+
'runtime/routes/twilio-routes.ts', // Twilio credential management (HTTP control-plane)
|
|
201
201
|
'security/token-manager.ts', // OAuth token refresh flow
|
|
202
202
|
'email/providers/index.ts', // email provider API key lookup
|
|
203
203
|
'tools/network/script-proxy/session-manager.ts', // proxy credential injection at runtime
|
|
@@ -299,6 +299,10 @@ mock.module('../memory/conversation-store.js', () => ({
|
|
|
299
299
|
getDisplayMetaForConversations: () => new Map(),
|
|
300
300
|
}));
|
|
301
301
|
|
|
302
|
+
mock.module('../runtime/confirmation-request-guardian-bridge.js', () => ({
|
|
303
|
+
bridgeConfirmationRequestToGuardian: () => ({ skipped: true, reason: 'not_trusted_contact' }),
|
|
304
|
+
}));
|
|
305
|
+
|
|
302
306
|
mock.module('../daemon/session.js', () => ({
|
|
303
307
|
Session: MockSession,
|
|
304
308
|
DEFAULT_MEMORY_POLICY: MOCK_DEFAULT_MEMORY_POLICY,
|
|
@@ -58,12 +58,17 @@ import {
|
|
|
58
58
|
import { initializeDb, resetDb } from '../memory/db.js';
|
|
59
59
|
import { getDb } from '../memory/db.js';
|
|
60
60
|
import { conversations } from '../memory/schema.js';
|
|
61
|
+
import type { ServerWithRequestIP } from '../runtime/middleware/actor-token.js';
|
|
61
62
|
import {
|
|
62
63
|
handleGuardianActionDecision,
|
|
63
64
|
handleGuardianActionsPending,
|
|
64
65
|
listGuardianDecisionPrompts,
|
|
65
66
|
} from '../runtime/routes/guardian-action-routes.js';
|
|
66
67
|
|
|
68
|
+
const mockLoopbackServer: ServerWithRequestIP = {
|
|
69
|
+
requestIP: () => ({ address: '127.0.0.1', family: 'IPv4', port: 0 }),
|
|
70
|
+
};
|
|
71
|
+
|
|
67
72
|
initializeDb();
|
|
68
73
|
|
|
69
74
|
function ensureConversation(id: string): void {
|
|
@@ -145,7 +150,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
145
150
|
method: 'POST',
|
|
146
151
|
body: JSON.stringify({ action: 'approve_once' }),
|
|
147
152
|
});
|
|
148
|
-
const res = await handleGuardianActionDecision(req);
|
|
153
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
149
154
|
expect(res.status).toBe(400);
|
|
150
155
|
const body = await res.json();
|
|
151
156
|
expect(body.error.message).toContain('requestId');
|
|
@@ -156,7 +161,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
156
161
|
method: 'POST',
|
|
157
162
|
body: JSON.stringify({ requestId: 'req-1' }),
|
|
158
163
|
});
|
|
159
|
-
const res = await handleGuardianActionDecision(req);
|
|
164
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
160
165
|
expect(res.status).toBe(400);
|
|
161
166
|
const body = await res.json();
|
|
162
167
|
expect(body.error.message).toContain('action');
|
|
@@ -167,7 +172,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
167
172
|
method: 'POST',
|
|
168
173
|
body: JSON.stringify({ requestId: 'req-1', action: 'nuke_from_orbit' }),
|
|
169
174
|
});
|
|
170
|
-
const res = await handleGuardianActionDecision(req);
|
|
175
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
171
176
|
expect(res.status).toBe(400);
|
|
172
177
|
const body = await res.json();
|
|
173
178
|
expect(body.error.message).toContain('Invalid action');
|
|
@@ -180,7 +185,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
180
185
|
method: 'POST',
|
|
181
186
|
body: JSON.stringify({ requestId: 'nonexistent', action: 'approve_once' }),
|
|
182
187
|
});
|
|
183
|
-
const res = await handleGuardianActionDecision(req);
|
|
188
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
184
189
|
expect(res.status).toBe(404);
|
|
185
190
|
});
|
|
186
191
|
|
|
@@ -192,7 +197,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
192
197
|
method: 'POST',
|
|
193
198
|
body: JSON.stringify({ requestId: 'req-gd-1', action: 'approve_once' }),
|
|
194
199
|
});
|
|
195
|
-
const res = await handleGuardianActionDecision(req);
|
|
200
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
196
201
|
expect(res.status).toBe(200);
|
|
197
202
|
const body = await res.json();
|
|
198
203
|
expect(body.applied).toBe(true);
|
|
@@ -207,7 +212,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
207
212
|
method: 'POST',
|
|
208
213
|
body: JSON.stringify({ requestId: 'req-scope-1', action: 'approve_once', conversationId: 'conv-wrong' }),
|
|
209
214
|
});
|
|
210
|
-
const res = await handleGuardianActionDecision(req);
|
|
215
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
211
216
|
expect(res.status).toBe(404);
|
|
212
217
|
const body = await res.json();
|
|
213
218
|
expect(body.error.message).toContain('No pending guardian action');
|
|
@@ -222,7 +227,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
222
227
|
method: 'POST',
|
|
223
228
|
body: JSON.stringify({ requestId: 'req-scope-2', action: 'reject', conversationId: 'conv-match' }),
|
|
224
229
|
});
|
|
225
|
-
const res = await handleGuardianActionDecision(req);
|
|
230
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
226
231
|
expect(res.status).toBe(200);
|
|
227
232
|
const body = await res.json();
|
|
228
233
|
expect(body.applied).toBe(true);
|
|
@@ -236,7 +241,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
236
241
|
method: 'POST',
|
|
237
242
|
body: JSON.stringify({ requestId: 'req-scope-3', action: 'approve_once' }),
|
|
238
243
|
});
|
|
239
|
-
const res = await handleGuardianActionDecision(req);
|
|
244
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
240
245
|
expect(res.status).toBe(200);
|
|
241
246
|
});
|
|
242
247
|
|
|
@@ -254,7 +259,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
254
259
|
method: 'POST',
|
|
255
260
|
body: JSON.stringify({ requestId: 'req-access-1', action: 'approve_once' }),
|
|
256
261
|
});
|
|
257
|
-
const res = await handleGuardianActionDecision(req);
|
|
262
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
258
263
|
expect(res.status).toBe(200);
|
|
259
264
|
const body = await res.json();
|
|
260
265
|
expect(body.applied).toBe(true);
|
|
@@ -276,7 +281,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
276
281
|
method: 'POST',
|
|
277
282
|
body: JSON.stringify({ requestId: 'req-voice-access-1', action: 'approve_once' }),
|
|
278
283
|
});
|
|
279
|
-
const res = await handleGuardianActionDecision(req);
|
|
284
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
280
285
|
expect(res.status).toBe(200);
|
|
281
286
|
const body = await res.json();
|
|
282
287
|
expect(body.applied).toBe(true);
|
|
@@ -291,7 +296,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
291
296
|
method: 'POST',
|
|
292
297
|
body: JSON.stringify({ requestId: 'req-stale-1', action: 'approve_once' }),
|
|
293
298
|
});
|
|
294
|
-
const res = await handleGuardianActionDecision(req);
|
|
299
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
295
300
|
const body = await res.json();
|
|
296
301
|
expect(body.applied).toBe(false);
|
|
297
302
|
expect(body.reason).toBe('already_resolved');
|
|
@@ -307,7 +312,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
307
312
|
method: 'POST',
|
|
308
313
|
body: JSON.stringify({ requestId: 'req-actor-1', action: 'approve_once' }),
|
|
309
314
|
});
|
|
310
|
-
await handleGuardianActionDecision(req);
|
|
315
|
+
await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
311
316
|
const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
|
|
312
317
|
const actorContext = call.actorContext as Record<string, unknown>;
|
|
313
318
|
expect(actorContext.externalUserId).toBeUndefined();
|
|
@@ -325,7 +330,7 @@ describe('HTTP handleGuardianActionsPending', () => {
|
|
|
325
330
|
|
|
326
331
|
test('returns 400 when conversationId is missing', () => {
|
|
327
332
|
const req = new Request('http://localhost/v1/guardian-actions/pending');
|
|
328
|
-
const res = handleGuardianActionsPending(req);
|
|
333
|
+
const res = handleGuardianActionsPending(req, mockLoopbackServer);
|
|
329
334
|
expect(res.status).toBe(400);
|
|
330
335
|
});
|
|
331
336
|
|
|
@@ -337,7 +342,7 @@ describe('HTTP handleGuardianActionsPending', () => {
|
|
|
337
342
|
});
|
|
338
343
|
|
|
339
344
|
const req = new Request('http://localhost/v1/guardian-actions/pending?conversationId=conv-list');
|
|
340
|
-
const res = handleGuardianActionsPending(req);
|
|
345
|
+
const res = handleGuardianActionsPending(req, mockLoopbackServer);
|
|
341
346
|
expect(res.status).toBe(200);
|
|
342
347
|
|
|
343
348
|
// Verify the prompts directly via the shared helper
|
|
@@ -367,6 +367,11 @@ describe('guardian-dispatch', () => {
|
|
|
367
367
|
expect(request).toBeDefined();
|
|
368
368
|
expect(request!.tool_name).toBe('send_email');
|
|
369
369
|
expect(request!.input_digest).toBe('abc123def456');
|
|
370
|
+
|
|
371
|
+
const signalParams = emitCalls[0] as Record<string, unknown>;
|
|
372
|
+
const payload = signalParams.contextPayload as Record<string, unknown>;
|
|
373
|
+
expect(payload.requestKind).toBe('pending_question');
|
|
374
|
+
expect(payload.toolName).toBe('send_email');
|
|
370
375
|
});
|
|
371
376
|
|
|
372
377
|
test('omitting toolName and inputDigest stores null for informational ASK_GUARDIAN dispatches', async () => {
|
|
@@ -422,6 +427,9 @@ describe('guardian-dispatch', () => {
|
|
|
422
427
|
// The request was just created so there is 1 pending request for this session
|
|
423
428
|
expect(payload.activeGuardianRequestCount).toBe(1);
|
|
424
429
|
expect(payload.callSessionId).toBe(session.id);
|
|
430
|
+
expect(payload.requestKind).toBe('pending_question');
|
|
431
|
+
expect(payload.toolName).toBeUndefined();
|
|
432
|
+
expect(payload.pendingQuestionId).toBeUndefined();
|
|
425
433
|
});
|
|
426
434
|
|
|
427
435
|
test('repeated guardian questions in the same call each create per-request delivery rows even when sharing a conversation', async () => {
|
|
@@ -268,7 +268,7 @@ describe('startOutbound', () => {
|
|
|
268
268
|
|
|
269
269
|
describe('resendOutbound', () => {
|
|
270
270
|
test('returns no_active_session when no session exists', () => {
|
|
271
|
-
const result = resendOutbound({ channel: 'sms'
|
|
271
|
+
const result = resendOutbound({ channel: 'sms' });
|
|
272
272
|
expect(result.success).toBe(false);
|
|
273
273
|
expect(result.error).toBe('no_active_session');
|
|
274
274
|
});
|
|
@@ -339,7 +339,7 @@ describe('resendOutbound', () => {
|
|
|
339
339
|
|
|
340
340
|
describe('cancelOutbound', () => {
|
|
341
341
|
test('returns no_active_session when no session exists', () => {
|
|
342
|
-
const result = cancelOutbound({ channel: 'sms'
|
|
342
|
+
const result = cancelOutbound({ channel: 'sms' });
|
|
343
343
|
expect(result.success).toBe(false);
|
|
344
344
|
expect(result.error).toBe('no_active_session');
|
|
345
345
|
});
|
|
@@ -397,7 +397,7 @@ describe('HTTP route: handleResendOutbound', () => {
|
|
|
397
397
|
});
|
|
398
398
|
|
|
399
399
|
test('returns 400 for no_active_session', async () => {
|
|
400
|
-
const req = jsonRequest({ channel: 'sms'
|
|
400
|
+
const req = jsonRequest({ channel: 'sms' });
|
|
401
401
|
const resp = await handleResendOutbound(req);
|
|
402
402
|
expect(resp.status).toBe(400);
|
|
403
403
|
const body = await resp.json() as { error?: string };
|
|
@@ -439,7 +439,7 @@ describe('HTTP route: handleCancelOutbound', () => {
|
|
|
439
439
|
});
|
|
440
440
|
|
|
441
441
|
test('returns 400 for no_active_session', async () => {
|
|
442
|
-
const req = jsonRequest({ channel: 'sms'
|
|
442
|
+
const req = jsonRequest({ channel: 'sms' });
|
|
443
443
|
const resp = await handleCancelOutbound(req);
|
|
444
444
|
expect(resp.status).toBe(400);
|
|
445
445
|
const body = await resp.json() as { error?: string };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildGuardianCodeOnlyClarification,
|
|
5
|
+
buildGuardianDisambiguationExample,
|
|
6
|
+
buildGuardianDisambiguationLabel,
|
|
7
|
+
buildGuardianInvalidActionReply,
|
|
8
|
+
buildGuardianReplyDirective,
|
|
9
|
+
buildGuardianRequestCodeInstruction,
|
|
10
|
+
hasGuardianRequestCodeInstruction,
|
|
11
|
+
parseGuardianQuestionPayload,
|
|
12
|
+
resolveGuardianInstructionModeForRequest,
|
|
13
|
+
resolveGuardianInstructionModeFromFields,
|
|
14
|
+
resolveGuardianQuestionInstructionMode,
|
|
15
|
+
stripConflictingGuardianRequestInstructions,
|
|
16
|
+
} from '../notifications/guardian-question-mode.js';
|
|
17
|
+
|
|
18
|
+
describe('guardian-question-mode', () => {
|
|
19
|
+
test('parses pending_question payload as discriminated union', () => {
|
|
20
|
+
const parsed = parseGuardianQuestionPayload({
|
|
21
|
+
requestKind: 'pending_question',
|
|
22
|
+
requestId: 'req-1',
|
|
23
|
+
requestCode: 'A1B2C3',
|
|
24
|
+
questionText: 'What time works?',
|
|
25
|
+
callSessionId: 'call-1',
|
|
26
|
+
activeGuardianRequestCount: 2,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(parsed).not.toBeNull();
|
|
30
|
+
expect(parsed?.requestKind).toBe('pending_question');
|
|
31
|
+
if (!parsed || parsed.requestKind !== 'pending_question') return;
|
|
32
|
+
expect(parsed.callSessionId).toBe('call-1');
|
|
33
|
+
expect(parsed.activeGuardianRequestCount).toBe(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('parses tool_grant_request payload and requires toolName', () => {
|
|
37
|
+
const parsed = parseGuardianQuestionPayload({
|
|
38
|
+
requestKind: 'tool_grant_request',
|
|
39
|
+
requestId: 'req-2',
|
|
40
|
+
requestCode: 'D4E5F6',
|
|
41
|
+
questionText: 'Allow host bash?',
|
|
42
|
+
toolName: 'host_bash',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(parsed).not.toBeNull();
|
|
46
|
+
expect(parsed?.requestKind).toBe('tool_grant_request');
|
|
47
|
+
if (!parsed || parsed.requestKind !== 'tool_grant_request') return;
|
|
48
|
+
expect(parsed.toolName).toBe('host_bash');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('parses pending_question payload with optional toolName metadata', () => {
|
|
52
|
+
const parsed = parseGuardianQuestionPayload({
|
|
53
|
+
requestKind: 'pending_question',
|
|
54
|
+
requestId: 'req-voice-tool-1',
|
|
55
|
+
requestCode: 'AA11BB',
|
|
56
|
+
questionText: 'Allow send_email?',
|
|
57
|
+
callSessionId: 'call-voice-1',
|
|
58
|
+
activeGuardianRequestCount: 1,
|
|
59
|
+
toolName: 'send_email',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(parsed).not.toBeNull();
|
|
63
|
+
expect(parsed?.requestKind).toBe('pending_question');
|
|
64
|
+
if (!parsed || parsed.requestKind !== 'pending_question') return;
|
|
65
|
+
expect(parsed.toolName).toBe('send_email');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('rejects invalid pending_question payload missing required fields', () => {
|
|
69
|
+
const parsed = parseGuardianQuestionPayload({
|
|
70
|
+
requestKind: 'pending_question',
|
|
71
|
+
requestId: 'req-3',
|
|
72
|
+
requestCode: 'AAA111',
|
|
73
|
+
questionText: 'Missing call session and count',
|
|
74
|
+
});
|
|
75
|
+
expect(parsed).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('resolve mode uses discriminant for valid typed payloads', () => {
|
|
79
|
+
const resolved = resolveGuardianQuestionInstructionMode({
|
|
80
|
+
requestKind: 'pending_question',
|
|
81
|
+
requestId: 'req-1',
|
|
82
|
+
requestCode: 'A1B2C3',
|
|
83
|
+
questionText: 'What time works?',
|
|
84
|
+
callSessionId: 'call-1',
|
|
85
|
+
activeGuardianRequestCount: 2,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(resolved.mode).toBe('answer');
|
|
89
|
+
expect(resolved.requestKind).toBe('pending_question');
|
|
90
|
+
expect(resolved.legacyFallbackUsed).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('resolve mode uses legacy fallback when requestKind is missing', () => {
|
|
94
|
+
const resolved = resolveGuardianQuestionInstructionMode({
|
|
95
|
+
requestCode: 'A1B2C3',
|
|
96
|
+
questionText: 'Allow host bash?',
|
|
97
|
+
toolName: 'host_bash',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(resolved.mode).toBe('approval');
|
|
101
|
+
expect(resolved.requestKind).toBeNull();
|
|
102
|
+
expect(resolved.legacyFallbackUsed).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('resolve mode treats pending_question with toolName as approval-mode', () => {
|
|
106
|
+
const resolved = resolveGuardianQuestionInstructionMode({
|
|
107
|
+
requestKind: 'pending_question',
|
|
108
|
+
requestId: 'req-voice-tool-2',
|
|
109
|
+
requestCode: 'CC22DD',
|
|
110
|
+
questionText: 'Allow send_email?',
|
|
111
|
+
callSessionId: 'call-voice-2',
|
|
112
|
+
activeGuardianRequestCount: 1,
|
|
113
|
+
toolName: 'send_email',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(resolved.mode).toBe('approval');
|
|
117
|
+
expect(resolved.requestKind).toBe('pending_question');
|
|
118
|
+
expect(resolved.legacyFallbackUsed).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('resolveGuardianInstructionModeFromFields returns null for unknown request kind', () => {
|
|
122
|
+
const resolved = resolveGuardianInstructionModeFromFields('unknown_kind', 'send_email');
|
|
123
|
+
expect(resolved).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('answer-mode instruction detection rejects approval phrasing', () => {
|
|
127
|
+
const code = 'A1B2C3';
|
|
128
|
+
const wrongInstruction = buildGuardianRequestCodeInstruction(code, 'approval');
|
|
129
|
+
const correctInstruction = buildGuardianRequestCodeInstruction(code, 'answer');
|
|
130
|
+
|
|
131
|
+
expect(hasGuardianRequestCodeInstruction(wrongInstruction, code, 'answer')).toBe(false);
|
|
132
|
+
expect(hasGuardianRequestCodeInstruction(correctInstruction, code, 'answer')).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('buildGuardianReplyDirective uses mode-specific wording', () => {
|
|
136
|
+
expect(buildGuardianReplyDirective('A1B2C3', 'approval')).toBe('Reply "A1B2C3 approve" or "A1B2C3 reject".');
|
|
137
|
+
expect(buildGuardianReplyDirective('A1B2C3', 'answer')).toBe('Reply "A1B2C3 <your answer>".');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('resolveGuardianInstructionModeForRequest handles tool-backed pending_question as approval', () => {
|
|
141
|
+
expect(
|
|
142
|
+
resolveGuardianInstructionModeForRequest({
|
|
143
|
+
kind: 'pending_question',
|
|
144
|
+
toolName: 'send_email',
|
|
145
|
+
}),
|
|
146
|
+
).toBe('approval');
|
|
147
|
+
expect(
|
|
148
|
+
resolveGuardianInstructionModeForRequest({
|
|
149
|
+
kind: 'pending_question',
|
|
150
|
+
toolName: null,
|
|
151
|
+
}),
|
|
152
|
+
).toBe('answer');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('centralized guardian response copy builders produce mode-specific copy', () => {
|
|
156
|
+
expect(buildGuardianInvalidActionReply('approval', 'A1B2C3')).toContain('approve');
|
|
157
|
+
expect(buildGuardianInvalidActionReply('answer', 'A1B2C3')).toContain('<your answer>');
|
|
158
|
+
|
|
159
|
+
expect(
|
|
160
|
+
buildGuardianCodeOnlyClarification('approval', {
|
|
161
|
+
requestCode: 'A1B2C3',
|
|
162
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
163
|
+
toolName: 'send_email',
|
|
164
|
+
}),
|
|
165
|
+
).toContain('I found request A1B2C3 for send_email.');
|
|
166
|
+
expect(
|
|
167
|
+
buildGuardianCodeOnlyClarification('answer', {
|
|
168
|
+
requestCode: 'A1B2C3',
|
|
169
|
+
questionText: 'What time works best?',
|
|
170
|
+
}),
|
|
171
|
+
).toContain('I found question A1B2C3.');
|
|
172
|
+
|
|
173
|
+
expect(
|
|
174
|
+
buildGuardianDisambiguationLabel('approval', {
|
|
175
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
176
|
+
toolName: 'send_email',
|
|
177
|
+
}),
|
|
178
|
+
).toBe('send_email');
|
|
179
|
+
expect(
|
|
180
|
+
buildGuardianDisambiguationLabel('answer', {
|
|
181
|
+
questionText: 'What time works best?',
|
|
182
|
+
}),
|
|
183
|
+
).toBe('What time works best?');
|
|
184
|
+
|
|
185
|
+
expect(buildGuardianDisambiguationExample('approval', 'A1B2C3')).toBe(
|
|
186
|
+
'For approvals: reply "A1B2C3 approve" or "A1B2C3 reject".',
|
|
187
|
+
);
|
|
188
|
+
expect(buildGuardianDisambiguationExample('answer', 'A1B2C3')).toBe(
|
|
189
|
+
'For questions: reply "A1B2C3 <your answer>".',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('stripConflictingGuardianRequestInstructions removes opposite-mode instructions', () => {
|
|
194
|
+
const approvalText = 'Reference code: A1B2C3. Reply "A1B2C3 approve" or "A1B2C3 reject".';
|
|
195
|
+
const answerText = 'Reference code: A1B2C3. Reply "A1B2C3 <your answer>".';
|
|
196
|
+
|
|
197
|
+
expect(stripConflictingGuardianRequestInstructions(approvalText, 'A1B2C3', 'answer')).toBe('');
|
|
198
|
+
expect(stripConflictingGuardianRequestInstructions(answerText, 'A1B2C3', 'approval')).toBe('');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -475,6 +475,69 @@ describe('routing invariant: code-only messages return clarification', () => {
|
|
|
475
475
|
expect(unchanged!.status).toBe('pending');
|
|
476
476
|
});
|
|
477
477
|
|
|
478
|
+
test('code-only pending_question asks for free-text answer (not approve/reject)', async () => {
|
|
479
|
+
const req = createCanonicalGuardianRequest({
|
|
480
|
+
kind: 'pending_question',
|
|
481
|
+
sourceType: 'voice',
|
|
482
|
+
sourceChannel: 'voice',
|
|
483
|
+
conversationId: 'conv-1',
|
|
484
|
+
guardianExternalUserId: 'guardian-1',
|
|
485
|
+
callSessionId: 'call-1',
|
|
486
|
+
pendingQuestionId: 'pq-1',
|
|
487
|
+
requestCode: 'A2B3C4',
|
|
488
|
+
questionText: 'What time works best?',
|
|
489
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const result = await routeGuardianReply(replyCtx({
|
|
493
|
+
messageText: 'A2B3C4',
|
|
494
|
+
conversationId: 'conv-1',
|
|
495
|
+
}));
|
|
496
|
+
|
|
497
|
+
expect(result.consumed).toBe(true);
|
|
498
|
+
expect(result.type).toBe('code_only_clarification');
|
|
499
|
+
expect(result.decisionApplied).toBe(false);
|
|
500
|
+
expect(result.replyText).toContain('A2B3C4');
|
|
501
|
+
expect(result.replyText).toContain('<your answer>');
|
|
502
|
+
expect(result.replyText).not.toContain('approve');
|
|
503
|
+
expect(result.replyText).not.toContain('reject');
|
|
504
|
+
|
|
505
|
+
const unchanged = getCanonicalGuardianRequest(req.id);
|
|
506
|
+
expect(unchanged!.status).toBe('pending');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('code-only tool-backed pending_question asks for approve/reject decision', async () => {
|
|
510
|
+
const req = createCanonicalGuardianRequest({
|
|
511
|
+
kind: 'pending_question',
|
|
512
|
+
sourceType: 'voice',
|
|
513
|
+
sourceChannel: 'voice',
|
|
514
|
+
conversationId: 'conv-1',
|
|
515
|
+
guardianExternalUserId: 'guardian-1',
|
|
516
|
+
callSessionId: 'call-2',
|
|
517
|
+
pendingQuestionId: 'pq-2',
|
|
518
|
+
requestCode: 'B2C3D4',
|
|
519
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
520
|
+
toolName: 'send_email',
|
|
521
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const result = await routeGuardianReply(replyCtx({
|
|
525
|
+
messageText: 'B2C3D4',
|
|
526
|
+
conversationId: 'conv-1',
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
expect(result.consumed).toBe(true);
|
|
530
|
+
expect(result.type).toBe('code_only_clarification');
|
|
531
|
+
expect(result.decisionApplied).toBe(false);
|
|
532
|
+
expect(result.replyText).toContain('B2C3D4');
|
|
533
|
+
expect(result.replyText).toContain('approve');
|
|
534
|
+
expect(result.replyText).toContain('reject');
|
|
535
|
+
expect(result.replyText).not.toContain('<your answer>');
|
|
536
|
+
|
|
537
|
+
const unchanged = getCanonicalGuardianRequest(req.id);
|
|
538
|
+
expect(unchanged!.status).toBe('pending');
|
|
539
|
+
});
|
|
540
|
+
|
|
478
541
|
test('code with decision text does apply the decision', async () => {
|
|
479
542
|
const req = createCanonicalGuardianRequest({
|
|
480
543
|
kind: 'tool_approval',
|
|
@@ -701,6 +764,51 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
701
764
|
expect(result.replyText).toContain('BBB222');
|
|
702
765
|
});
|
|
703
766
|
|
|
767
|
+
test('disambiguation treats tool-backed pending_question as approval request', async () => {
|
|
768
|
+
const answerRequest = createCanonicalGuardianRequest({
|
|
769
|
+
kind: 'pending_question',
|
|
770
|
+
sourceType: 'voice',
|
|
771
|
+
sourceChannel: 'voice',
|
|
772
|
+
conversationId: 'conv-1',
|
|
773
|
+
guardianExternalUserId: 'guardian-1',
|
|
774
|
+
callSessionId: 'call-answer',
|
|
775
|
+
pendingQuestionId: 'pq-answer',
|
|
776
|
+
requestCode: 'ABC123',
|
|
777
|
+
questionText: 'What time works best?',
|
|
778
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const approvalRequest = createCanonicalGuardianRequest({
|
|
782
|
+
kind: 'pending_question',
|
|
783
|
+
sourceType: 'voice',
|
|
784
|
+
sourceChannel: 'voice',
|
|
785
|
+
conversationId: 'conv-1',
|
|
786
|
+
guardianExternalUserId: 'guardian-1',
|
|
787
|
+
callSessionId: 'call-approval',
|
|
788
|
+
pendingQuestionId: 'pq-approval',
|
|
789
|
+
requestCode: 'DEF456',
|
|
790
|
+
questionText: 'Allow send_email to bob@example.com?',
|
|
791
|
+
toolName: 'send_email',
|
|
792
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const result = await routeGuardianReply(replyCtx({
|
|
796
|
+
messageText: 'approve',
|
|
797
|
+
conversationId: 'conv-guardian-thread',
|
|
798
|
+
pendingRequestIds: [answerRequest.id, approvalRequest.id],
|
|
799
|
+
approvalConversationGenerator: undefined,
|
|
800
|
+
}));
|
|
801
|
+
|
|
802
|
+
expect(result.consumed).toBe(true);
|
|
803
|
+
expect(result.type).toBe('disambiguation_needed');
|
|
804
|
+
expect(result.decisionApplied).toBe(false);
|
|
805
|
+
expect(result.replyText).toContain('ABC123');
|
|
806
|
+
expect(result.replyText).toContain('DEF456');
|
|
807
|
+
expect(result.replyText).toContain('send_email');
|
|
808
|
+
expect(result.replyText).toContain('For questions: reply "ABC123 <your answer>".');
|
|
809
|
+
expect(result.replyText).toContain('For approvals: reply "DEF456 approve" or "DEF456 reject".');
|
|
810
|
+
});
|
|
811
|
+
|
|
704
812
|
test('single pending request does not need disambiguation', async () => {
|
|
705
813
|
const req = createCanonicalGuardianRequest({
|
|
706
814
|
kind: 'tool_approval',
|
|
@@ -1130,4 +1238,74 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1130
1238
|
const resolved = getCanonicalGuardianRequest(req.id);
|
|
1131
1239
|
expect(resolved!.status).toBe('approved');
|
|
1132
1240
|
});
|
|
1241
|
+
|
|
1242
|
+
test('trusted desktop access-request approval returns a verification code reply', async () => {
|
|
1243
|
+
const req = createCanonicalGuardianRequest({
|
|
1244
|
+
kind: 'access_request',
|
|
1245
|
+
sourceType: 'channel',
|
|
1246
|
+
sourceChannel: 'telegram',
|
|
1247
|
+
conversationId: 'conv-access-desktop',
|
|
1248
|
+
guardianExternalUserId: 'guardian-1',
|
|
1249
|
+
requestCode: 'C0D3A5',
|
|
1250
|
+
toolName: 'ingress_access_request',
|
|
1251
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
const result = await routeGuardianReply({
|
|
1255
|
+
messageText: 'C0D3A5 approve',
|
|
1256
|
+
channel: 'vellum',
|
|
1257
|
+
actor: trustedActor({ channel: 'vellum' }),
|
|
1258
|
+
conversationId: 'conv-guardian-thread',
|
|
1259
|
+
pendingRequestIds: [req.id],
|
|
1260
|
+
approvalConversationGenerator: undefined,
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
expect(result.consumed).toBe(true);
|
|
1264
|
+
expect(result.decisionApplied).toBe(true);
|
|
1265
|
+
expect(result.replyText).toContain('verification code');
|
|
1266
|
+
expect(result.replyText).toMatch(/\b\d{6}\b/);
|
|
1267
|
+
|
|
1268
|
+
const resolved = getCanonicalGuardianRequest(req.id);
|
|
1269
|
+
expect(resolved!.status).toBe('approved');
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
test('NL decision path preserves resolver verification code reply text', async () => {
|
|
1273
|
+
const req = createCanonicalGuardianRequest({
|
|
1274
|
+
kind: 'access_request',
|
|
1275
|
+
sourceType: 'channel',
|
|
1276
|
+
sourceChannel: 'telegram',
|
|
1277
|
+
conversationId: 'conv-access-desktop-nl',
|
|
1278
|
+
guardianExternalUserId: 'guardian-1',
|
|
1279
|
+
requesterExternalUserId: 'requester-1',
|
|
1280
|
+
requesterChatId: 'chat-1',
|
|
1281
|
+
requestCode: 'A1B2C3',
|
|
1282
|
+
toolName: 'ingress_access_request',
|
|
1283
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
const approvalConversationGenerator = async () => ({
|
|
1287
|
+
disposition: 'approve_once' as const,
|
|
1288
|
+
replyText: 'Access approved.',
|
|
1289
|
+
targetRequestId: req.id,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const result = await routeGuardianReply({
|
|
1293
|
+
messageText: 'please approve this request',
|
|
1294
|
+
channel: 'vellum',
|
|
1295
|
+
actor: trustedActor({ channel: 'vellum' }),
|
|
1296
|
+
conversationId: 'conv-guardian-thread',
|
|
1297
|
+
pendingRequestIds: [req.id],
|
|
1298
|
+
approvalConversationGenerator: approvalConversationGenerator as any,
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
expect(result.consumed).toBe(true);
|
|
1302
|
+
expect(result.decisionApplied).toBe(true);
|
|
1303
|
+
expect(result.type).toBe('canonical_decision_applied');
|
|
1304
|
+
expect(result.replyText).toContain('verification code');
|
|
1305
|
+
expect(result.replyText).toMatch(/\b\d{6}\b/);
|
|
1306
|
+
expect(result.replyText).not.toBe('Access approved.');
|
|
1307
|
+
|
|
1308
|
+
const resolved = getCanonicalGuardianRequest(req.id);
|
|
1309
|
+
expect(resolved!.status).toBe('approved');
|
|
1310
|
+
});
|
|
1133
1311
|
});
|