@vellumai/assistant 0.4.2 → 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 +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- 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__/access-request-decision.test.ts +0 -1
- 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 +415 -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__/call-routes-http.test.ts +0 -25
- 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 -86
- 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 +6 -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__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- 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__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -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 +1475 -33
- 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 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -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__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- 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 +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- 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 +309 -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 +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- 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 +8 -1
- 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 +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -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/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- 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 +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- 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/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- 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 +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- 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 +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -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 +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- 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-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- 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/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
const routeGuardianReplyMock = mock(async () => ({
|
|
4
|
+
consumed: false,
|
|
5
|
+
decisionApplied: false,
|
|
6
|
+
type: 'not_consumed' as const,
|
|
7
|
+
})) as any;
|
|
8
|
+
const listPendingByDestinationMock = mock((_conversationId: string, _sourceChannel?: string) => [] as Array<{ id: string; kind?: string }>);
|
|
9
|
+
const listCanonicalMock = mock((_filters?: Record<string, unknown>) => [] as Array<{ id: string }>);
|
|
10
|
+
const addMessageMock = mock(async (_conversationId: string, role: string, _content?: string, _metadata?: Record<string, unknown>) => ({
|
|
11
|
+
id: role === 'user' ? 'persisted-user-id' : 'persisted-assistant-id',
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module('../util/logger.js', () => ({
|
|
15
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
16
|
+
get: () => () => {},
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module('../memory/conversation-key-store.js', () => ({
|
|
21
|
+
getOrCreateConversation: () => ({ conversationId: 'conv-canonical-reply' }),
|
|
22
|
+
getConversationByKey: () => null,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module('../memory/attachments-store.js', () => ({
|
|
26
|
+
getAttachmentsByIds: () => [],
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
mock.module('../runtime/guardian-reply-router.js', () => ({
|
|
30
|
+
routeGuardianReply: routeGuardianReplyMock,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module('../memory/canonical-guardian-store.js', () => ({
|
|
34
|
+
createCanonicalGuardianRequest: () => ({ id: 'canonical-id', requestCode: 'ABC123' }),
|
|
35
|
+
generateCanonicalRequestCode: () => 'ABC123',
|
|
36
|
+
listPendingCanonicalGuardianRequestsByDestinationConversation: (
|
|
37
|
+
conversationId: string,
|
|
38
|
+
sourceChannel?: string,
|
|
39
|
+
) => listPendingByDestinationMock(conversationId, sourceChannel),
|
|
40
|
+
listCanonicalGuardianRequests: (filters?: Record<string, unknown>) =>
|
|
41
|
+
listCanonicalMock(filters),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
mock.module('../runtime/confirmation-request-guardian-bridge.js', () => ({
|
|
45
|
+
bridgeConfirmationRequestToGuardian: async () => undefined,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
mock.module('../memory/conversation-store.js', () => ({
|
|
49
|
+
addMessage: (
|
|
50
|
+
conversationId: string,
|
|
51
|
+
role: string,
|
|
52
|
+
content: string,
|
|
53
|
+
metadata?: Record<string, unknown>,
|
|
54
|
+
) => addMessageMock(conversationId, role, content, metadata),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
mock.module('../runtime/local-actor-identity.js', () => ({
|
|
58
|
+
resolveLocalIpcGuardianContext: () => ({ trustClass: 'guardian', sourceChannel: 'vellum' }),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
import { handleSendMessage } from '../runtime/routes/conversation-routes.js';
|
|
62
|
+
|
|
63
|
+
const testServer = {
|
|
64
|
+
requestIP: () => ({ address: '127.0.0.1' }),
|
|
65
|
+
} as unknown as import('../runtime/middleware/actor-token.js').ServerWithRequestIP;
|
|
66
|
+
|
|
67
|
+
describe('handleSendMessage canonical guardian reply interception', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
routeGuardianReplyMock.mockClear();
|
|
70
|
+
listPendingByDestinationMock.mockClear();
|
|
71
|
+
listCanonicalMock.mockClear();
|
|
72
|
+
addMessageMock.mockClear();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('consumes access-request code replies on desktop HTTP path without pending confirmations', async () => {
|
|
76
|
+
listPendingByDestinationMock.mockReturnValue([{ id: 'access-req-1' }]);
|
|
77
|
+
listCanonicalMock.mockReturnValue([]);
|
|
78
|
+
routeGuardianReplyMock.mockResolvedValue({
|
|
79
|
+
consumed: true,
|
|
80
|
+
decisionApplied: true,
|
|
81
|
+
type: 'canonical_decision_applied',
|
|
82
|
+
requestId: 'access-req-1',
|
|
83
|
+
replyText: 'Access approved. Verification code: 123456.',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const persistUserMessage = mock(async () => 'should-not-be-called');
|
|
87
|
+
const runAgentLoop = mock(async () => undefined);
|
|
88
|
+
const session = {
|
|
89
|
+
setGuardianContext: () => {},
|
|
90
|
+
setStateSignalListener: () => {},
|
|
91
|
+
emitConfirmationStateChanged: () => {},
|
|
92
|
+
emitActivityState: () => {},
|
|
93
|
+
setTurnChannelContext: () => {},
|
|
94
|
+
setTurnInterfaceContext: () => {},
|
|
95
|
+
isProcessing: () => false,
|
|
96
|
+
hasAnyPendingConfirmation: () => false,
|
|
97
|
+
denyAllPendingConfirmations: () => {},
|
|
98
|
+
enqueueMessage: () => ({ queued: true, requestId: 'queued-id' }),
|
|
99
|
+
persistUserMessage,
|
|
100
|
+
runAgentLoop,
|
|
101
|
+
getMessages: () => [] as unknown[],
|
|
102
|
+
assistantId: 'self',
|
|
103
|
+
guardianContext: undefined,
|
|
104
|
+
hasPendingConfirmation: () => false,
|
|
105
|
+
} as unknown as import('../daemon/session.js').Session;
|
|
106
|
+
|
|
107
|
+
const req = new Request('http://localhost/v1/messages', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
conversationKey: 'guardian-thread-key',
|
|
112
|
+
content: '05BECB approve',
|
|
113
|
+
sourceChannel: 'vellum',
|
|
114
|
+
interface: 'macos',
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const res = await handleSendMessage(req, {
|
|
119
|
+
sendMessageDeps: {
|
|
120
|
+
getOrCreateSession: async () => session,
|
|
121
|
+
assistantEventHub: { publish: async () => {} } as any,
|
|
122
|
+
resolveAttachments: () => [],
|
|
123
|
+
},
|
|
124
|
+
}, testServer);
|
|
125
|
+
|
|
126
|
+
expect(res.status).toBe(202);
|
|
127
|
+
const body = await res.json() as { accepted: boolean; messageId?: string };
|
|
128
|
+
expect(body.accepted).toBe(true);
|
|
129
|
+
expect(body.messageId).toBe('persisted-user-id');
|
|
130
|
+
|
|
131
|
+
expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
|
|
132
|
+
const routerCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
|
|
133
|
+
expect(routerCall.messageText).toBe('05BECB approve');
|
|
134
|
+
expect(routerCall.pendingRequestIds).toEqual(['access-req-1']);
|
|
135
|
+
expect(addMessageMock).toHaveBeenCalledTimes(2);
|
|
136
|
+
expect(persistUserMessage).toHaveBeenCalledTimes(0);
|
|
137
|
+
expect(runAgentLoop).toHaveBeenCalledTimes(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('passes undefined pendingRequestIds when no canonical hints are found', async () => {
|
|
141
|
+
listPendingByDestinationMock.mockReturnValue([]);
|
|
142
|
+
listCanonicalMock.mockReturnValue([]);
|
|
143
|
+
routeGuardianReplyMock.mockResolvedValue({
|
|
144
|
+
consumed: false,
|
|
145
|
+
decisionApplied: false,
|
|
146
|
+
type: 'not_consumed',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const persistUserMessage = mock(async () => 'persisted-user-id');
|
|
150
|
+
const runAgentLoop = mock(async () => undefined);
|
|
151
|
+
const session = {
|
|
152
|
+
setGuardianContext: () => {},
|
|
153
|
+
setStateSignalListener: () => {},
|
|
154
|
+
emitConfirmationStateChanged: () => {},
|
|
155
|
+
emitActivityState: () => {},
|
|
156
|
+
setTurnChannelContext: () => {},
|
|
157
|
+
setTurnInterfaceContext: () => {},
|
|
158
|
+
isProcessing: () => false,
|
|
159
|
+
hasAnyPendingConfirmation: () => false,
|
|
160
|
+
denyAllPendingConfirmations: () => {},
|
|
161
|
+
enqueueMessage: () => ({ queued: true, requestId: 'queued-id' }),
|
|
162
|
+
persistUserMessage,
|
|
163
|
+
runAgentLoop,
|
|
164
|
+
getMessages: () => [] as unknown[],
|
|
165
|
+
assistantId: 'self',
|
|
166
|
+
guardianContext: undefined,
|
|
167
|
+
hasPendingConfirmation: () => false,
|
|
168
|
+
} as unknown as import('../daemon/session.js').Session;
|
|
169
|
+
|
|
170
|
+
const req = new Request('http://localhost/v1/messages', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
conversationKey: 'guardian-thread-key',
|
|
175
|
+
content: 'hello there',
|
|
176
|
+
sourceChannel: 'vellum',
|
|
177
|
+
interface: 'macos',
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const res = await handleSendMessage(req, {
|
|
182
|
+
sendMessageDeps: {
|
|
183
|
+
getOrCreateSession: async () => session,
|
|
184
|
+
assistantEventHub: { publish: async () => {} } as any,
|
|
185
|
+
resolveAttachments: () => [],
|
|
186
|
+
},
|
|
187
|
+
}, testServer);
|
|
188
|
+
|
|
189
|
+
expect(res.status).toBe(202);
|
|
190
|
+
expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
|
|
191
|
+
const routerCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
|
|
192
|
+
expect(routerCall.pendingRequestIds).toBeUndefined();
|
|
193
|
+
expect(persistUserMessage).toHaveBeenCalledTimes(1);
|
|
194
|
+
expect(runAgentLoop).toHaveBeenCalledTimes(1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('excludes stale tool_approval hints without a live pending confirmation', async () => {
|
|
198
|
+
listPendingByDestinationMock.mockReturnValue([
|
|
199
|
+
{ id: 'tool-approval-live', kind: 'tool_approval' },
|
|
200
|
+
{ id: 'tool-approval-stale', kind: 'tool_approval' },
|
|
201
|
+
{ id: 'access-req-1', kind: 'access_request' },
|
|
202
|
+
]);
|
|
203
|
+
listCanonicalMock.mockReturnValue([]);
|
|
204
|
+
routeGuardianReplyMock.mockResolvedValue({
|
|
205
|
+
consumed: false,
|
|
206
|
+
decisionApplied: false,
|
|
207
|
+
type: 'not_consumed',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const persistUserMessage = mock(async () => 'persisted-user-id');
|
|
211
|
+
const runAgentLoop = mock(async () => undefined);
|
|
212
|
+
const session = {
|
|
213
|
+
setGuardianContext: () => {},
|
|
214
|
+
setStateSignalListener: () => {},
|
|
215
|
+
emitConfirmationStateChanged: () => {},
|
|
216
|
+
emitActivityState: () => {},
|
|
217
|
+
setTurnChannelContext: () => {},
|
|
218
|
+
setTurnInterfaceContext: () => {},
|
|
219
|
+
isProcessing: () => false,
|
|
220
|
+
hasAnyPendingConfirmation: () => true,
|
|
221
|
+
denyAllPendingConfirmations: () => {},
|
|
222
|
+
enqueueMessage: () => ({ queued: true, requestId: 'queued-id' }),
|
|
223
|
+
persistUserMessage,
|
|
224
|
+
runAgentLoop,
|
|
225
|
+
getMessages: () => [] as unknown[],
|
|
226
|
+
assistantId: 'self',
|
|
227
|
+
guardianContext: undefined,
|
|
228
|
+
hasPendingConfirmation: (requestId: string) => requestId === 'tool-approval-live',
|
|
229
|
+
} as unknown as import('../daemon/session.js').Session;
|
|
230
|
+
|
|
231
|
+
const req = new Request('http://localhost/v1/messages', {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
conversationKey: 'guardian-thread-key',
|
|
236
|
+
content: 'approve',
|
|
237
|
+
sourceChannel: 'vellum',
|
|
238
|
+
interface: 'macos',
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const res = await handleSendMessage(req, {
|
|
243
|
+
sendMessageDeps: {
|
|
244
|
+
getOrCreateSession: async () => session,
|
|
245
|
+
assistantEventHub: { publish: async () => {} } as any,
|
|
246
|
+
resolveAttachments: () => [],
|
|
247
|
+
},
|
|
248
|
+
}, testServer);
|
|
249
|
+
|
|
250
|
+
expect(res.status).toBe(202);
|
|
251
|
+
expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
|
|
252
|
+
const routerCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
|
|
253
|
+
expect(routerCall.pendingRequestIds).toEqual(['tool-approval-live', 'access-req-1']);
|
|
254
|
+
expect((routerCall.pendingRequestIds as string[]).includes('tool-approval-stale')).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -17,8 +17,17 @@ mock.module('../memory/attachments-store.js', () => ({
|
|
|
17
17
|
getAttachmentsByIds: () => [],
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
mock.module('../runtime/local-actor-identity.js', () => ({
|
|
21
|
+
resolveLocalIpcGuardianContext: (sourceChannel: string) => ({ trustClass: 'guardian', sourceChannel }),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import type { ServerWithRequestIP } from '../runtime/middleware/actor-token.js';
|
|
20
25
|
import { handleSendMessage } from '../runtime/routes/conversation-routes.js';
|
|
21
26
|
|
|
27
|
+
const mockLoopbackServer: ServerWithRequestIP = {
|
|
28
|
+
requestIP: () => ({ address: '127.0.0.1', family: 'IPv4', port: 0 }),
|
|
29
|
+
};
|
|
30
|
+
|
|
22
31
|
describe('handleSendMessage', () => {
|
|
23
32
|
test('legacy fallback passes guardian context to processor', async () => {
|
|
24
33
|
let capturedOptions: RuntimeMessageSessionOptions | undefined;
|
|
@@ -41,16 +50,16 @@ describe('handleSendMessage', () => {
|
|
|
41
50
|
capturedSourceChannel = sourceChannel;
|
|
42
51
|
return { messageId: 'msg-legacy-fallback' };
|
|
43
52
|
},
|
|
44
|
-
});
|
|
53
|
+
}, mockLoopbackServer);
|
|
45
54
|
|
|
46
55
|
const body = await res.json() as { accepted: boolean; messageId: string };
|
|
47
56
|
expect(res.status).toBe(202);
|
|
48
57
|
expect(body.accepted).toBe(true);
|
|
49
58
|
expect(body.messageId).toBe('msg-legacy-fallback');
|
|
50
59
|
expect(capturedSourceChannel).toBe('telegram');
|
|
51
|
-
expect(capturedOptions?.guardianContext).toEqual({
|
|
60
|
+
expect(capturedOptions?.guardianContext).toEqual(expect.objectContaining({
|
|
52
61
|
trustClass: 'guardian',
|
|
53
62
|
sourceChannel: 'telegram',
|
|
54
|
-
});
|
|
63
|
+
}));
|
|
55
64
|
});
|
|
56
65
|
});
|
|
@@ -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,
|
|
@@ -32,7 +32,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
32
32
|
getDbPath: () => join(testDir, 'test.db'),
|
|
33
33
|
getLogPath: () => join(testDir, 'test.log'),
|
|
34
34
|
ensureDataDir: () => {},
|
|
35
|
-
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
36
35
|
readHttpToken: () => 'test-bearer-token',
|
|
37
36
|
}));
|
|
38
37
|
|
|
@@ -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);
|
|
@@ -262,6 +267,27 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
262
267
|
expect(mockApplyCanonicalGuardianDecision).toHaveBeenCalledTimes(1);
|
|
263
268
|
});
|
|
264
269
|
|
|
270
|
+
test('applies decision for voice access_request kind through canonical primitive', async () => {
|
|
271
|
+
createTestCanonicalRequest({
|
|
272
|
+
conversationId: 'conv-voice-access',
|
|
273
|
+
requestId: 'req-voice-access-1',
|
|
274
|
+
kind: 'access_request',
|
|
275
|
+
toolName: 'ingress_access_request',
|
|
276
|
+
guardianExternalUserId: 'guardian-voice-42',
|
|
277
|
+
});
|
|
278
|
+
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-voice-access-1', grantMinted: false });
|
|
279
|
+
|
|
280
|
+
const req = new Request('http://localhost/v1/guardian-actions/decision', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
body: JSON.stringify({ requestId: 'req-voice-access-1', action: 'approve_once' }),
|
|
283
|
+
});
|
|
284
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
285
|
+
expect(res.status).toBe(200);
|
|
286
|
+
const body = await res.json();
|
|
287
|
+
expect(body.applied).toBe(true);
|
|
288
|
+
expect(mockApplyCanonicalGuardianDecision).toHaveBeenCalledTimes(1);
|
|
289
|
+
});
|
|
290
|
+
|
|
265
291
|
test('returns stale reason from canonical decision primitive', async () => {
|
|
266
292
|
createTestCanonicalRequest({ conversationId: 'conv-stale', requestId: 'req-stale-1' });
|
|
267
293
|
mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: false, reason: 'already_resolved' });
|
|
@@ -270,7 +296,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
270
296
|
method: 'POST',
|
|
271
297
|
body: JSON.stringify({ requestId: 'req-stale-1', action: 'approve_once' }),
|
|
272
298
|
});
|
|
273
|
-
const res = await handleGuardianActionDecision(req);
|
|
299
|
+
const res = await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
274
300
|
const body = await res.json();
|
|
275
301
|
expect(body.applied).toBe(false);
|
|
276
302
|
expect(body.reason).toBe('already_resolved');
|
|
@@ -286,7 +312,7 @@ describe('HTTP handleGuardianActionDecision', () => {
|
|
|
286
312
|
method: 'POST',
|
|
287
313
|
body: JSON.stringify({ requestId: 'req-actor-1', action: 'approve_once' }),
|
|
288
314
|
});
|
|
289
|
-
await handleGuardianActionDecision(req);
|
|
315
|
+
await handleGuardianActionDecision(req, mockLoopbackServer);
|
|
290
316
|
const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
|
|
291
317
|
const actorContext = call.actorContext as Record<string, unknown>;
|
|
292
318
|
expect(actorContext.externalUserId).toBeUndefined();
|
|
@@ -304,7 +330,7 @@ describe('HTTP handleGuardianActionsPending', () => {
|
|
|
304
330
|
|
|
305
331
|
test('returns 400 when conversationId is missing', () => {
|
|
306
332
|
const req = new Request('http://localhost/v1/guardian-actions/pending');
|
|
307
|
-
const res = handleGuardianActionsPending(req);
|
|
333
|
+
const res = handleGuardianActionsPending(req, mockLoopbackServer);
|
|
308
334
|
expect(res.status).toBe(400);
|
|
309
335
|
});
|
|
310
336
|
|
|
@@ -316,7 +342,7 @@ describe('HTTP handleGuardianActionsPending', () => {
|
|
|
316
342
|
});
|
|
317
343
|
|
|
318
344
|
const req = new Request('http://localhost/v1/guardian-actions/pending?conversationId=conv-list');
|
|
319
|
-
const res = handleGuardianActionsPending(req);
|
|
345
|
+
const res = handleGuardianActionsPending(req, mockLoopbackServer);
|
|
320
346
|
expect(res.status).toBe(200);
|
|
321
347
|
|
|
322
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 () => {
|
|
@@ -35,7 +35,6 @@ mock.module('../util/platform.js', () => ({
|
|
|
35
35
|
getDbPath: () => join(testDir, 'test.db'),
|
|
36
36
|
getLogPath: () => join(testDir, 'test.log'),
|
|
37
37
|
ensureDataDir: () => {},
|
|
38
|
-
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
39
38
|
readHttpToken: () => 'test-bearer-token',
|
|
40
39
|
}));
|
|
41
40
|
|
|
@@ -269,7 +268,7 @@ describe('startOutbound', () => {
|
|
|
269
268
|
|
|
270
269
|
describe('resendOutbound', () => {
|
|
271
270
|
test('returns no_active_session when no session exists', () => {
|
|
272
|
-
const result = resendOutbound({ channel: 'sms'
|
|
271
|
+
const result = resendOutbound({ channel: 'sms' });
|
|
273
272
|
expect(result.success).toBe(false);
|
|
274
273
|
expect(result.error).toBe('no_active_session');
|
|
275
274
|
});
|
|
@@ -340,7 +339,7 @@ describe('resendOutbound', () => {
|
|
|
340
339
|
|
|
341
340
|
describe('cancelOutbound', () => {
|
|
342
341
|
test('returns no_active_session when no session exists', () => {
|
|
343
|
-
const result = cancelOutbound({ channel: 'sms'
|
|
342
|
+
const result = cancelOutbound({ channel: 'sms' });
|
|
344
343
|
expect(result.success).toBe(false);
|
|
345
344
|
expect(result.error).toBe('no_active_session');
|
|
346
345
|
});
|
|
@@ -398,7 +397,7 @@ describe('HTTP route: handleResendOutbound', () => {
|
|
|
398
397
|
});
|
|
399
398
|
|
|
400
399
|
test('returns 400 for no_active_session', async () => {
|
|
401
|
-
const req = jsonRequest({ channel: 'sms'
|
|
400
|
+
const req = jsonRequest({ channel: 'sms' });
|
|
402
401
|
const resp = await handleResendOutbound(req);
|
|
403
402
|
expect(resp.status).toBe(400);
|
|
404
403
|
const body = await resp.json() as { error?: string };
|
|
@@ -440,7 +439,7 @@ describe('HTTP route: handleCancelOutbound', () => {
|
|
|
440
439
|
});
|
|
441
440
|
|
|
442
441
|
test('returns 400 for no_active_session', async () => {
|
|
443
|
-
const req = jsonRequest({ channel: 'sms'
|
|
442
|
+
const req = jsonRequest({ channel: 'sms' });
|
|
444
443
|
const resp = await handleCancelOutbound(req);
|
|
445
444
|
expect(resp.status).toBe(400);
|
|
446
445
|
const body = await resp.json() as { error?: string };
|