@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
|
@@ -461,6 +461,57 @@ describe('AgentLoop', () => {
|
|
|
461
461
|
expect(warningBlock).toBeDefined();
|
|
462
462
|
});
|
|
463
463
|
|
|
464
|
+
test('runs without limit when maxToolUseTurns is 0', async () => {
|
|
465
|
+
// Use 20 turns (beyond old default of 8 used in other tests) to verify no cap
|
|
466
|
+
const turnCount = 20;
|
|
467
|
+
const responses: ProviderResponse[] = [];
|
|
468
|
+
for (let i = 0; i < turnCount; i++) {
|
|
469
|
+
responses.push(toolUseResponse(`t${i}`, 'read_file', { path: `/${i}.txt` }));
|
|
470
|
+
}
|
|
471
|
+
responses.push(textResponse('done'));
|
|
472
|
+
const { provider, calls } = createMockProvider(responses);
|
|
473
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
474
|
+
const loop = new AgentLoop(
|
|
475
|
+
provider,
|
|
476
|
+
'system',
|
|
477
|
+
{ maxToolUseTurns: 0, minTurnIntervalMs: 0 },
|
|
478
|
+
dummyTools,
|
|
479
|
+
toolExecutor,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const events: AgentEvent[] = [];
|
|
483
|
+
await loop.run([userMessage], collectEvents(events));
|
|
484
|
+
|
|
485
|
+
// All 20 tool turns + 1 final text response = 21 provider calls
|
|
486
|
+
expect(calls).toHaveLength(turnCount + 1);
|
|
487
|
+
|
|
488
|
+
// No hard-limit error events should have been emitted
|
|
489
|
+
const errorEvents = events.filter(
|
|
490
|
+
(e): e is Extract<AgentEvent, { type: 'error' }> => e.type === 'error',
|
|
491
|
+
);
|
|
492
|
+
expect(errorEvents).toHaveLength(0);
|
|
493
|
+
|
|
494
|
+
// Progress check reminders should still fire every 5 turns
|
|
495
|
+
const progressChecks = calls.filter((call) => {
|
|
496
|
+
const lastMsg = call.messages[call.messages.length - 1];
|
|
497
|
+
return lastMsg.content.some(
|
|
498
|
+
(b): b is Extract<ContentBlock, { type: 'text' }> =>
|
|
499
|
+
b.type === 'text' && b.text.includes('making meaningful progress'),
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
expect(progressChecks.length).toBeGreaterThanOrEqual(3);
|
|
503
|
+
|
|
504
|
+
// No approaching-limit warnings should appear
|
|
505
|
+
const limitWarnings = calls.filter((call) => {
|
|
506
|
+
const lastMsg = call.messages[call.messages.length - 1];
|
|
507
|
+
return lastMsg.content.some(
|
|
508
|
+
(b): b is Extract<ContentBlock, { type: 'text' }> =>
|
|
509
|
+
b.type === 'text' && b.text.includes('approaching the tool-use turn limit'),
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
expect(limitWarnings).toHaveLength(0);
|
|
513
|
+
});
|
|
514
|
+
|
|
464
515
|
// 9. Tool executor error results are forwarded correctly
|
|
465
516
|
test('forwards tool error results to provider', async () => {
|
|
466
517
|
const { provider, calls } = createMockProvider([
|
|
@@ -83,6 +83,7 @@ function makeIdleSession(opts?: {
|
|
|
83
83
|
setCommandIntent: () => {},
|
|
84
84
|
setTurnChannelContext: () => {},
|
|
85
85
|
setTurnInterfaceContext: () => {},
|
|
86
|
+
setStateSignalListener: () => {},
|
|
86
87
|
updateClient: () => {},
|
|
87
88
|
enqueueMessage: () => ({ queued: false, requestId: 'noop' }),
|
|
88
89
|
hasAnyPendingConfirmation: () => false,
|
|
@@ -125,6 +126,7 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
125
126
|
setCommandIntent: () => {},
|
|
126
127
|
setTurnChannelContext: () => {},
|
|
127
128
|
setTurnInterfaceContext: () => {},
|
|
129
|
+
setStateSignalListener: () => {},
|
|
128
130
|
updateClient: () => {},
|
|
129
131
|
enqueueMessage: () => ({ queued: false, requestId: 'noop' }),
|
|
130
132
|
hasAnyPendingConfirmation: () => false,
|
|
@@ -150,7 +150,7 @@ describe('SSE route — capacity limit', () => {
|
|
|
150
150
|
|
|
151
151
|
test('new connection evicts oldest and returns 200', async () => {
|
|
152
152
|
const hub = new AssistantEventHub({ maxSubscribers: 1 });
|
|
153
|
-
const opts = { hub, heartbeatIntervalMs: 60_000 };
|
|
153
|
+
const opts = { hub, heartbeatIntervalMs: 60_000, skipActorVerification: true as const };
|
|
154
154
|
|
|
155
155
|
const ac1 = new AbortController();
|
|
156
156
|
const req1 = new Request('http://localhost/v1/events?conversationKey=evict-a', { signal: ac1.signal });
|
|
@@ -181,7 +181,7 @@ describe('SSE route — capacity limit', () => {
|
|
|
181
181
|
{ signal: new AbortController().signal },
|
|
182
182
|
);
|
|
183
183
|
|
|
184
|
-
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub });
|
|
184
|
+
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub, skipActorVerification: true });
|
|
185
185
|
expect(response.status).toBe(503);
|
|
186
186
|
const body = await response.json() as { error: { message: string; code?: string } };
|
|
187
187
|
expect(body.error.message).toMatch(/Too many concurrent connections/);
|
|
@@ -195,7 +195,7 @@ describe('SSE route — capacity limit', () => {
|
|
|
195
195
|
{ signal: ac.signal },
|
|
196
196
|
);
|
|
197
197
|
|
|
198
|
-
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub });
|
|
198
|
+
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub, skipActorVerification: true });
|
|
199
199
|
|
|
200
200
|
expect(response.status).toBe(200);
|
|
201
201
|
ac.abort(); // clean up the subscription
|
|
@@ -218,6 +218,7 @@ describe('SSE route — heartbeat', () => {
|
|
|
218
218
|
const response = handleSubscribeAssistantEvents(req, new URL(req.url), {
|
|
219
219
|
hub,
|
|
220
220
|
heartbeatIntervalMs: 10,
|
|
221
|
+
skipActorVerification: true,
|
|
221
222
|
});
|
|
222
223
|
|
|
223
224
|
// Wait for at least one heartbeat interval to fire.
|
|
@@ -243,6 +244,7 @@ describe('SSE route — heartbeat', () => {
|
|
|
243
244
|
const response = handleSubscribeAssistantEvents(req, new URL(req.url), {
|
|
244
245
|
hub,
|
|
245
246
|
heartbeatIntervalMs: 10,
|
|
247
|
+
skipActorVerification: true,
|
|
246
248
|
});
|
|
247
249
|
|
|
248
250
|
// Wait for several intervals.
|
|
@@ -283,7 +285,7 @@ describe('SSE route — disconnect cleanup', () => {
|
|
|
283
285
|
{ signal: ac.signal },
|
|
284
286
|
);
|
|
285
287
|
|
|
286
|
-
handleSubscribeAssistantEvents(req, new URL(req.url), { hub });
|
|
288
|
+
handleSubscribeAssistantEvents(req, new URL(req.url), { hub, skipActorVerification: true });
|
|
287
289
|
|
|
288
290
|
expect(hub.subscriberCount()).toBe(1);
|
|
289
291
|
|
|
@@ -303,7 +305,7 @@ describe('SSE route — disconnect cleanup', () => {
|
|
|
303
305
|
{ signal: ac.signal },
|
|
304
306
|
);
|
|
305
307
|
|
|
306
|
-
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub });
|
|
308
|
+
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub, skipActorVerification: true });
|
|
307
309
|
|
|
308
310
|
expect(hub.subscriberCount()).toBe(1);
|
|
309
311
|
|
|
@@ -287,4 +287,129 @@ describe('assistant ID boundary', () => {
|
|
|
287
287
|
// all daemon storage keyed by DAEMON_INTERNAL_ASSISTANT_ID uses the fixed
|
|
288
288
|
// internal value rather than externally-provided IDs).
|
|
289
289
|
// -------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
// -------------------------------------------------------------------------
|
|
292
|
+
// Rule (e): No assistantId on daemon control-plane request/param types
|
|
293
|
+
//
|
|
294
|
+
// Daemon IPC contracts and guardian outbound param interfaces must not
|
|
295
|
+
// accept an assistantId field -- the daemon always uses
|
|
296
|
+
// DAEMON_INTERNAL_ASSISTANT_ID internally. Accepting assistantId on these
|
|
297
|
+
// surfaces invites callers to pass external IDs into daemon scoping.
|
|
298
|
+
// -------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
test('IPC contract types do not contain assistantId for guardian requests', () => {
|
|
301
|
+
const ipcContractPath = join(import.meta.dir, '..', 'daemon', 'ipc-contract', 'integrations.ts');
|
|
302
|
+
const content = readFileSync(ipcContractPath, 'utf-8');
|
|
303
|
+
|
|
304
|
+
// Extract the interface blocks for the request types and verify
|
|
305
|
+
// none of them declare an assistantId property.
|
|
306
|
+
const requestTypeNames = [
|
|
307
|
+
'GuardianVerificationRequest',
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
for (const typeName of requestTypeNames) {
|
|
311
|
+
// Find the interface/type block — match from the type name to the next
|
|
312
|
+
// closing brace at the same indentation level. We use a simple heuristic:
|
|
313
|
+
// find the line declaring the type, then scan forward to the closing '}'.
|
|
314
|
+
const typeIndex = content.indexOf(typeName);
|
|
315
|
+
expect(typeIndex, `Expected to find ${typeName} in IPC contract`).toBeGreaterThan(-1);
|
|
316
|
+
|
|
317
|
+
// Extract from the type declaration to the next '}' line
|
|
318
|
+
const blockStart = content.indexOf('{', typeIndex);
|
|
319
|
+
if (blockStart === -1) continue;
|
|
320
|
+
let braceDepth = 0;
|
|
321
|
+
let blockEnd = blockStart;
|
|
322
|
+
for (let i = blockStart; i < content.length; i++) {
|
|
323
|
+
if (content[i] === '{') braceDepth++;
|
|
324
|
+
if (content[i] === '}') braceDepth--;
|
|
325
|
+
if (braceDepth === 0) {
|
|
326
|
+
blockEnd = i + 1;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const block = content.slice(blockStart, blockEnd);
|
|
331
|
+
|
|
332
|
+
// The block should not contain an assistantId property declaration
|
|
333
|
+
// (matches "assistantId?" or "assistantId:" on a non-comment line)
|
|
334
|
+
const lines = block.split('\n');
|
|
335
|
+
for (const line of lines) {
|
|
336
|
+
const trimmed = line.trim();
|
|
337
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
338
|
+
expect(
|
|
339
|
+
/\bassistantId\s*[?:]/.test(trimmed),
|
|
340
|
+
`${typeName} must not declare an assistantId property. Found: "${trimmed}"`,
|
|
341
|
+
).toBe(false);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('guardian outbound param interfaces do not contain assistantId', () => {
|
|
347
|
+
const actionsPath = join(import.meta.dir, '..', 'runtime', 'guardian-outbound-actions.ts');
|
|
348
|
+
const content = readFileSync(actionsPath, 'utf-8');
|
|
349
|
+
|
|
350
|
+
const interfaceNames = [
|
|
351
|
+
'StartOutboundParams',
|
|
352
|
+
'ResendOutboundParams',
|
|
353
|
+
'CancelOutboundParams',
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
for (const name of interfaceNames) {
|
|
357
|
+
const idx = content.indexOf(name);
|
|
358
|
+
expect(idx, `Expected to find ${name} in guardian-outbound-actions.ts`).toBeGreaterThan(-1);
|
|
359
|
+
|
|
360
|
+
const blockStart = content.indexOf('{', idx);
|
|
361
|
+
if (blockStart === -1) continue;
|
|
362
|
+
let braceDepth = 0;
|
|
363
|
+
let blockEnd = blockStart;
|
|
364
|
+
for (let i = blockStart; i < content.length; i++) {
|
|
365
|
+
if (content[i] === '{') braceDepth++;
|
|
366
|
+
if (content[i] === '}') braceDepth--;
|
|
367
|
+
if (braceDepth === 0) {
|
|
368
|
+
blockEnd = i + 1;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const block = content.slice(blockStart, blockEnd);
|
|
373
|
+
|
|
374
|
+
const lines = block.split('\n');
|
|
375
|
+
for (const line of lines) {
|
|
376
|
+
const trimmed = line.trim();
|
|
377
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
378
|
+
expect(
|
|
379
|
+
/\bassistantId\s*[?:]/.test(trimmed),
|
|
380
|
+
`${name} must not declare an assistantId property. Found: "${trimmed}"`,
|
|
381
|
+
).toBe(false);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('channel readiness service does not accept assistantId parameter', () => {
|
|
387
|
+
const servicePath = join(import.meta.dir, '..', 'runtime', 'channel-readiness-service.ts');
|
|
388
|
+
const content = readFileSync(servicePath, 'utf-8');
|
|
389
|
+
|
|
390
|
+
// getReadiness and invalidateChannel signatures must not include assistantId
|
|
391
|
+
const signaturePatterns = [
|
|
392
|
+
/getReadiness\([^)]*assistantId/,
|
|
393
|
+
/invalidateChannel\([^)]*assistantId/,
|
|
394
|
+
];
|
|
395
|
+
for (const pattern of signaturePatterns) {
|
|
396
|
+
expect(
|
|
397
|
+
pattern.test(content),
|
|
398
|
+
`Channel readiness service must not accept assistantId parameter (matched: ${pattern})`,
|
|
399
|
+
).toBe(false);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ChannelProbeContext must not have assistantId.
|
|
403
|
+
// The interface is declared in channel-readiness-types.ts, not the service file.
|
|
404
|
+
const typesPath = join(import.meta.dir, '..', 'runtime', 'channel-readiness-types.ts');
|
|
405
|
+
const typesContent = readFileSync(typesPath, 'utf-8');
|
|
406
|
+
const probeContextMatch = typesContent.match(/interface\s+ChannelProbeContext\s*\{([^}]*)\}/);
|
|
407
|
+
expect(probeContextMatch, 'Expected to find ChannelProbeContext interface in channel-readiness-types.ts').not.toBeNull();
|
|
408
|
+
if (probeContextMatch) {
|
|
409
|
+
expect(
|
|
410
|
+
probeContextMatch[1],
|
|
411
|
+
'ChannelProbeContext must not contain assistantId',
|
|
412
|
+
).not.toContain('assistantId');
|
|
413
|
+
}
|
|
414
|
+
});
|
|
290
415
|
});
|
|
@@ -56,10 +56,12 @@ mock.module('../config/loader.js', () => ({
|
|
|
56
56
|
// ── Call constants mock ──────────────────────────────────────────────
|
|
57
57
|
|
|
58
58
|
let mockConsultationTimeoutMs = 90_000;
|
|
59
|
+
let mockSilenceTimeoutMs = 30_000;
|
|
59
60
|
|
|
60
61
|
mock.module('../calls/call-constants.js', () => ({
|
|
61
62
|
getMaxCallDurationMs: () => 12 * 60 * 1000,
|
|
62
63
|
getUserConsultationTimeoutMs: () => mockConsultationTimeoutMs,
|
|
64
|
+
getSilenceTimeoutMs: () => mockSilenceTimeoutMs,
|
|
63
65
|
SILENCE_TIMEOUT_MS: 30_000,
|
|
64
66
|
MAX_CALL_DURATION_MS: 3600 * 1000,
|
|
65
67
|
USER_CONSULTATION_TIMEOUT_MS: 120 * 1000,
|
|
@@ -154,6 +156,7 @@ interface MockRelay extends RelayConnection {
|
|
|
154
156
|
sentTokens: Array<{ token: string; last: boolean }>;
|
|
155
157
|
endCalled: boolean;
|
|
156
158
|
endReason: string | undefined;
|
|
159
|
+
mockConnectionState: string;
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
function createMockRelay(): MockRelay {
|
|
@@ -161,12 +164,15 @@ function createMockRelay(): MockRelay {
|
|
|
161
164
|
sentTokens: [] as Array<{ token: string; last: boolean }>,
|
|
162
165
|
_endCalled: false,
|
|
163
166
|
_endReason: undefined as string | undefined,
|
|
167
|
+
_connectionState: 'connected',
|
|
164
168
|
};
|
|
165
169
|
|
|
166
170
|
return {
|
|
167
171
|
get sentTokens() { return state.sentTokens; },
|
|
168
172
|
get endCalled() { return state._endCalled; },
|
|
169
173
|
get endReason() { return state._endReason; },
|
|
174
|
+
get mockConnectionState() { return state._connectionState; },
|
|
175
|
+
set mockConnectionState(v: string) { state._connectionState = v; },
|
|
170
176
|
sendTextToken(token: string, last: boolean) {
|
|
171
177
|
state.sentTokens.push({ token, last });
|
|
172
178
|
},
|
|
@@ -174,6 +180,9 @@ function createMockRelay(): MockRelay {
|
|
|
174
180
|
state._endCalled = true;
|
|
175
181
|
state._endReason = reason;
|
|
176
182
|
},
|
|
183
|
+
getConnectionState() {
|
|
184
|
+
return state._connectionState;
|
|
185
|
+
},
|
|
177
186
|
} as unknown as MockRelay;
|
|
178
187
|
}
|
|
179
188
|
|
|
@@ -236,6 +245,7 @@ describe('call-controller', () => {
|
|
|
236
245
|
mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Hello', ' there']));
|
|
237
246
|
// Reset consultation timeout to the default (long) value
|
|
238
247
|
mockConsultationTimeoutMs = 90_000;
|
|
248
|
+
mockSilenceTimeoutMs = 30_000;
|
|
239
249
|
});
|
|
240
250
|
|
|
241
251
|
// ── handleCallerUtterance ─────────────────────────────────────────
|
|
@@ -1697,4 +1707,43 @@ describe('call-controller', () => {
|
|
|
1697
1707
|
|
|
1698
1708
|
controller.destroy();
|
|
1699
1709
|
});
|
|
1710
|
+
|
|
1711
|
+
// ── Silence suppression during guardian wait ──────────────────────
|
|
1712
|
+
|
|
1713
|
+
test('silence timeout suppressed during guardian wait: does not say "Are you still there?"', async () => {
|
|
1714
|
+
mockSilenceTimeoutMs = 50; // Short timeout for testing
|
|
1715
|
+
const { relay, controller } = setupController();
|
|
1716
|
+
|
|
1717
|
+
// Simulate guardian wait state on the relay
|
|
1718
|
+
relay.mockConnectionState = 'awaiting_guardian_decision';
|
|
1719
|
+
|
|
1720
|
+
// Wait for the silence timeout to fire
|
|
1721
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1722
|
+
|
|
1723
|
+
// "Are you still there?" should NOT have been sent
|
|
1724
|
+
const silenceTokens = relay.sentTokens.filter((t) =>
|
|
1725
|
+
t.token.includes('Are you still there?'),
|
|
1726
|
+
);
|
|
1727
|
+
expect(silenceTokens.length).toBe(0);
|
|
1728
|
+
|
|
1729
|
+
controller.destroy();
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
test('silence timeout fires normally when not in guardian wait', async () => {
|
|
1733
|
+
mockSilenceTimeoutMs = 50; // Short timeout for testing
|
|
1734
|
+
const { relay, controller } = setupController();
|
|
1735
|
+
|
|
1736
|
+
// Default connection state is 'connected' (not guardian wait)
|
|
1737
|
+
|
|
1738
|
+
// Wait for the silence timeout to fire
|
|
1739
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1740
|
+
|
|
1741
|
+
// "Are you still there?" SHOULD have been sent
|
|
1742
|
+
const silenceTokens = relay.sentTokens.filter((t) =>
|
|
1743
|
+
t.token.includes('Are you still there?'),
|
|
1744
|
+
);
|
|
1745
|
+
expect(silenceTokens.length).toBe(1);
|
|
1746
|
+
|
|
1747
|
+
controller.destroy();
|
|
1748
|
+
});
|
|
1700
1749
|
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mock } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
mock.module('../util/logger.js', () => ({
|
|
5
|
+
getLogger: () =>
|
|
6
|
+
new Proxy({} as Record<string, unknown>, {
|
|
7
|
+
get: () => () => {},
|
|
8
|
+
}),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
buildPointerGenerationPrompt,
|
|
13
|
+
type CallPointerMessageContext,
|
|
14
|
+
composeCallPointerMessageGenerative,
|
|
15
|
+
getPointerFallbackMessage,
|
|
16
|
+
includesRequiredFacts,
|
|
17
|
+
} from '../calls/call-pointer-message-composer.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Deterministic fallback templates
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
describe('getPointerFallbackMessage', () => {
|
|
24
|
+
test('started without verification code', () => {
|
|
25
|
+
const msg = getPointerFallbackMessage({ scenario: 'started', phoneNumber: '+15551234567' });
|
|
26
|
+
expect(msg).toContain('Call to +15551234567 started');
|
|
27
|
+
expect(msg).not.toContain('Verification code');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('started with verification code', () => {
|
|
31
|
+
const msg = getPointerFallbackMessage({
|
|
32
|
+
scenario: 'started',
|
|
33
|
+
phoneNumber: '+15551234567',
|
|
34
|
+
verificationCode: '1234',
|
|
35
|
+
});
|
|
36
|
+
expect(msg).toContain('Verification code: 1234');
|
|
37
|
+
expect(msg).toContain('+15551234567');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('completed without duration', () => {
|
|
41
|
+
const msg = getPointerFallbackMessage({ scenario: 'completed', phoneNumber: '+15559876543' });
|
|
42
|
+
expect(msg).toContain('completed');
|
|
43
|
+
expect(msg).toContain('+15559876543');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('completed with duration', () => {
|
|
47
|
+
const msg = getPointerFallbackMessage({
|
|
48
|
+
scenario: 'completed',
|
|
49
|
+
phoneNumber: '+15559876543',
|
|
50
|
+
duration: '5m 30s',
|
|
51
|
+
});
|
|
52
|
+
expect(msg).toContain('completed (5m 30s)');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('failed without reason', () => {
|
|
56
|
+
const msg = getPointerFallbackMessage({ scenario: 'failed', phoneNumber: '+15559876543' });
|
|
57
|
+
expect(msg).toContain('failed');
|
|
58
|
+
expect(msg).toContain('+15559876543');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('failed with reason', () => {
|
|
62
|
+
const msg = getPointerFallbackMessage({
|
|
63
|
+
scenario: 'failed',
|
|
64
|
+
phoneNumber: '+15559876543',
|
|
65
|
+
reason: 'no answer',
|
|
66
|
+
});
|
|
67
|
+
expect(msg).toContain('failed: no answer');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('guardian_verification_succeeded defaults to voice channel', () => {
|
|
71
|
+
const msg = getPointerFallbackMessage({
|
|
72
|
+
scenario: 'guardian_verification_succeeded',
|
|
73
|
+
phoneNumber: '+15559876543',
|
|
74
|
+
});
|
|
75
|
+
expect(msg).toContain('Guardian verification (voice)');
|
|
76
|
+
expect(msg).toContain('succeeded');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('guardian_verification_succeeded with custom channel', () => {
|
|
80
|
+
const msg = getPointerFallbackMessage({
|
|
81
|
+
scenario: 'guardian_verification_succeeded',
|
|
82
|
+
phoneNumber: '+15559876543',
|
|
83
|
+
channel: 'sms',
|
|
84
|
+
});
|
|
85
|
+
expect(msg).toContain('Guardian verification (sms)');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('guardian_verification_failed without reason', () => {
|
|
89
|
+
const msg = getPointerFallbackMessage({
|
|
90
|
+
scenario: 'guardian_verification_failed',
|
|
91
|
+
phoneNumber: '+15559876543',
|
|
92
|
+
});
|
|
93
|
+
expect(msg).toContain('Guardian verification');
|
|
94
|
+
expect(msg).toContain('failed');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('guardian_verification_failed with reason', () => {
|
|
98
|
+
const msg = getPointerFallbackMessage({
|
|
99
|
+
scenario: 'guardian_verification_failed',
|
|
100
|
+
phoneNumber: '+15559876543',
|
|
101
|
+
reason: 'Max attempts exceeded',
|
|
102
|
+
});
|
|
103
|
+
expect(msg).toContain('failed: Max attempts exceeded');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Required facts validation
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
describe('includesRequiredFacts', () => {
|
|
112
|
+
test('returns true when no required facts', () => {
|
|
113
|
+
expect(includesRequiredFacts('any text', undefined)).toBe(true);
|
|
114
|
+
expect(includesRequiredFacts('any text', [])).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('returns true when all facts present', () => {
|
|
118
|
+
expect(includesRequiredFacts('Call to +15551234567 completed (2m).', ['+15551234567', '2m'])).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('returns false when a fact is missing', () => {
|
|
122
|
+
expect(includesRequiredFacts('Call completed.', ['+15551234567'])).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Prompt builder
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
describe('buildPointerGenerationPrompt', () => {
|
|
131
|
+
test('includes context JSON and fallback message', () => {
|
|
132
|
+
const ctx: CallPointerMessageContext = { scenario: 'started', phoneNumber: '+15551234567' };
|
|
133
|
+
const prompt = buildPointerGenerationPrompt(ctx, 'Fallback text', undefined);
|
|
134
|
+
expect(prompt).toContain(JSON.stringify(ctx));
|
|
135
|
+
expect(prompt).toContain('Fallback text');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('includes required facts clause when provided', () => {
|
|
139
|
+
const ctx: CallPointerMessageContext = { scenario: 'completed', phoneNumber: '+15559876543', duration: '3m' };
|
|
140
|
+
const prompt = buildPointerGenerationPrompt(ctx, 'Fallback', ['+15559876543', '3m']);
|
|
141
|
+
expect(prompt).toContain('Required facts to include');
|
|
142
|
+
expect(prompt).toContain('+15559876543');
|
|
143
|
+
expect(prompt).toContain('3m');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Generative composition (test env falls back to deterministic)
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe('composeCallPointerMessageGenerative', () => {
|
|
152
|
+
test('returns fallback in test environment regardless of generator', async () => {
|
|
153
|
+
const generator = async () => 'LLM-generated copy';
|
|
154
|
+
const ctx: CallPointerMessageContext = { scenario: 'started', phoneNumber: '+15551234567' };
|
|
155
|
+
const result = await composeCallPointerMessageGenerative(ctx, {}, generator);
|
|
156
|
+
// NODE_ENV is 'test' during bun test
|
|
157
|
+
expect(result).toContain('Call to +15551234567 started');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('returns fallback when no generator provided', async () => {
|
|
161
|
+
const ctx: CallPointerMessageContext = { scenario: 'failed', phoneNumber: '+15559876543', reason: 'busy' };
|
|
162
|
+
const result = await composeCallPointerMessageGenerative(ctx);
|
|
163
|
+
expect(result).toContain('failed: busy');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('uses custom fallbackText when provided', async () => {
|
|
167
|
+
const ctx: CallPointerMessageContext = { scenario: 'completed', phoneNumber: '+15559876543' };
|
|
168
|
+
const result = await composeCallPointerMessageGenerative(ctx, { fallbackText: 'Custom fallback' });
|
|
169
|
+
expect(result).toBe('Custom fallback');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
5
|
-
import { afterAll, beforeEach, describe, expect, mock,test } from 'bun:test';
|
|
5
|
+
import { afterAll, afterEach, beforeEach, describe, expect, mock,test } from 'bun:test';
|
|
6
6
|
|
|
7
7
|
const testDir = mkdtempSync(join(tmpdir(), 'call-pointer-messages-test-'));
|
|
8
8
|
|
|
@@ -25,14 +25,14 @@ mock.module('../util/logger.js', () => ({
|
|
|
25
25
|
}),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
-
import { addPointerMessage, formatDuration } from '../calls/call-pointer-messages.js';
|
|
28
|
+
import { addPointerMessage, formatDuration, resetPointerCopyGenerator, setPointerCopyGenerator } from '../calls/call-pointer-messages.js';
|
|
29
29
|
import { getMessages } from '../memory/conversation-store.js';
|
|
30
30
|
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
31
31
|
import { conversations } from '../memory/schema.js';
|
|
32
32
|
|
|
33
33
|
initializeDb();
|
|
34
34
|
|
|
35
|
-
function ensureConversation(id: string): void {
|
|
35
|
+
function ensureConversation(id: string, options?: { threadType?: string; originChannel?: string }): void {
|
|
36
36
|
const db = getDb();
|
|
37
37
|
const now = Date.now();
|
|
38
38
|
db.insert(conversations).values({
|
|
@@ -40,6 +40,8 @@ function ensureConversation(id: string): void {
|
|
|
40
40
|
title: `Conversation ${id}`,
|
|
41
41
|
createdAt: now,
|
|
42
42
|
updatedAt: now,
|
|
43
|
+
...(options?.threadType ? { threadType: options.threadType } : {}),
|
|
44
|
+
...(options?.originChannel ? { originChannel: options.originChannel } : {}),
|
|
43
45
|
}).run();
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -92,6 +94,10 @@ describe('addPointerMessage', () => {
|
|
|
92
94
|
resetTables();
|
|
93
95
|
});
|
|
94
96
|
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
resetPointerCopyGenerator();
|
|
99
|
+
});
|
|
100
|
+
|
|
95
101
|
afterAll(() => {
|
|
96
102
|
resetDb();
|
|
97
103
|
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
@@ -185,4 +191,88 @@ describe('addPointerMessage', () => {
|
|
|
185
191
|
const text = getLatestAssistantText(convId);
|
|
186
192
|
expect(text).toContain('failed: Max attempts exceeded');
|
|
187
193
|
});
|
|
194
|
+
|
|
195
|
+
// Trust-aware tests: in test env, generator is not called (NODE_ENV=test
|
|
196
|
+
// short-circuits to fallback), so these validate the trust gating path
|
|
197
|
+
// while still receiving deterministic text.
|
|
198
|
+
|
|
199
|
+
test('untrusted audience uses deterministic fallback even with generator set', () => {
|
|
200
|
+
const convId = 'conv-ptr-untrusted';
|
|
201
|
+
// standard threadType + no origin channel = untrusted
|
|
202
|
+
ensureConversation(convId, { threadType: 'standard' });
|
|
203
|
+
|
|
204
|
+
const generatorCalled = { value: false };
|
|
205
|
+
setPointerCopyGenerator(async () => {
|
|
206
|
+
generatorCalled.value = true;
|
|
207
|
+
return 'generated text';
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
addPointerMessage(convId, 'started', '+15551234567');
|
|
211
|
+
const text = getLatestAssistantText(convId);
|
|
212
|
+
// In test env, deterministic fallback is always used regardless of trust
|
|
213
|
+
expect(text).toContain('Call to +15551234567 started');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('explicit untrusted audience mode skips generator', () => {
|
|
217
|
+
const convId = 'conv-ptr-explicit-untrusted';
|
|
218
|
+
ensureConversation(convId, { threadType: 'private' });
|
|
219
|
+
|
|
220
|
+
const generatorCalled = { value: false };
|
|
221
|
+
setPointerCopyGenerator(async () => {
|
|
222
|
+
generatorCalled.value = true;
|
|
223
|
+
return 'generated text';
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
addPointerMessage(convId, 'started', '+15551234567', undefined, 'untrusted');
|
|
227
|
+
const text = getLatestAssistantText(convId);
|
|
228
|
+
expect(text).toContain('Call to +15551234567 started');
|
|
229
|
+
// generator is not called because audience is explicitly untrusted
|
|
230
|
+
expect(generatorCalled.value).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('private threadType is detected as trusted audience', async () => {
|
|
234
|
+
const convId = 'conv-ptr-private';
|
|
235
|
+
ensureConversation(convId, { threadType: 'private' });
|
|
236
|
+
|
|
237
|
+
setPointerCopyGenerator(async () => 'generated text');
|
|
238
|
+
|
|
239
|
+
await addPointerMessage(convId, 'completed', '+15559876543', { duration: '1m' });
|
|
240
|
+
const text = getLatestAssistantText(convId);
|
|
241
|
+
// In test env, falls back to deterministic even on trusted path
|
|
242
|
+
expect(text).toContain('Call to +15559876543 completed (1m)');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('vellum origin channel is detected as trusted audience', async () => {
|
|
246
|
+
const convId = 'conv-ptr-vellum';
|
|
247
|
+
ensureConversation(convId, { originChannel: 'vellum' });
|
|
248
|
+
|
|
249
|
+
setPointerCopyGenerator(async () => 'generated text');
|
|
250
|
+
|
|
251
|
+
await addPointerMessage(convId, 'failed', '+15559876543', { reason: 'busy' });
|
|
252
|
+
const text = getLatestAssistantText(convId);
|
|
253
|
+
expect(text).toContain('failed: busy');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('missing conversation defaults to untrusted', () => {
|
|
257
|
+
const _convId = 'conv-ptr-missing';
|
|
258
|
+
// Don't create the conversation — trust resolution should default to untrusted
|
|
259
|
+
|
|
260
|
+
const generatorCalled = { value: false };
|
|
261
|
+
setPointerCopyGenerator(async () => {
|
|
262
|
+
generatorCalled.value = true;
|
|
263
|
+
return 'generated text';
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// This will fail at addMessage because conversation doesn't exist,
|
|
267
|
+
// but the trust check itself should not throw. Test just the trust
|
|
268
|
+
// gating by using a conversation that exists but has no trust signals.
|
|
269
|
+
const convId2 = 'conv-ptr-no-signals';
|
|
270
|
+
ensureConversation(convId2);
|
|
271
|
+
|
|
272
|
+
addPointerMessage(convId2, 'started', '+15551234567');
|
|
273
|
+
const text = getLatestAssistantText(convId2);
|
|
274
|
+
expect(text).toContain('Call to +15551234567 started');
|
|
275
|
+
// generator not called because standard threadType + no origin = untrusted
|
|
276
|
+
expect(generatorCalled.value).toBe(false);
|
|
277
|
+
});
|
|
188
278
|
});
|