@vellumai/assistant 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +27 -10
- package/README.md +6 -6
- package/bun.lock +57 -2
- package/docs/architecture/memory.md +4 -4
- package/docs/trusted-contact-access.md +8 -0
- package/package.json +3 -2
- package/src/__tests__/actor-token-service.test.ts +9 -6
- package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/call-controller.test.ts +115 -0
- package/src/__tests__/call-domain.test.ts +148 -10
- package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
- package/src/__tests__/call-pointer-messages.test.ts +105 -43
- package/src/__tests__/canonical-guardian-store.test.ts +44 -10
- package/src/__tests__/channel-approval-routes.test.ts +67 -65
- package/src/__tests__/channel-delivery-store.test.ts +2 -2
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
- package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
- package/src/__tests__/gateway-client-managed-outbound.test.ts +147 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
- package/src/__tests__/guardian-dispatch.test.ts +39 -1
- package/src/__tests__/guardian-grant-minting.test.ts +24 -24
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
- package/src/__tests__/guardian-routing-state.test.ts +10 -32
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
- package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
- package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +57 -47
- package/src/__tests__/notification-decision-fallback.test.ts +232 -0
- package/src/__tests__/notification-decision-strategy.test.ts +304 -8
- package/src/__tests__/notification-guardian-path.test.ts +38 -1
- package/src/__tests__/relay-server.test.ts +136 -5
- package/src/__tests__/send-endpoint-busy.test.ts +35 -1
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
- package/src/__tests__/system-prompt.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +1 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +10 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +14 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +24 -24
- package/src/__tests__/trusted-contact-multichannel.test.ts +5 -5
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/approvals/guardian-decision-primitive.ts +29 -25
- package/src/approvals/guardian-request-resolvers.ts +9 -5
- package/src/calls/call-controller.ts +15 -0
- package/src/calls/call-pointer-message-composer.ts +27 -85
- package/src/calls/call-pointer-messages.ts +54 -21
- package/src/calls/guardian-dispatch.ts +30 -0
- package/src/calls/relay-server.ts +58 -24
- package/src/calls/types.ts +1 -0
- package/src/config/system-prompt.ts +10 -3
- package/src/config/templates/BOOTSTRAP.md +6 -5
- package/src/config/templates/USER.md +1 -0
- package/src/config/user-reference.ts +44 -0
- package/src/daemon/handlers/guardian-actions.ts +5 -2
- package/src/daemon/handlers/sessions.ts +8 -3
- package/src/daemon/lifecycle.ts +109 -3
- package/src/daemon/providers-setup.ts +0 -8
- package/src/daemon/server.ts +32 -24
- package/src/daemon/session-agent-loop.ts +4 -3
- package/src/daemon/session-lifecycle.ts +1 -9
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-runtime-assembly.ts +2 -0
- package/src/daemon/session-slash.ts +35 -2
- package/src/daemon/session-tool-setup.ts +10 -0
- package/src/daemon/session.ts +1 -0
- package/src/memory/canonical-guardian-store.ts +40 -0
- package/src/memory/conversation-crud.ts +26 -0
- package/src/memory/conversation-store.ts +1 -0
- package/src/memory/db-init.ts +12 -0
- package/src/memory/guardian-bindings.ts +4 -0
- package/src/memory/job-handlers/backfill.ts +2 -9
- package/src/memory/migrations/039-actor-refresh-token-records.ts +51 -0
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema.ts +22 -0
- package/src/notifications/README.md +8 -1
- package/src/notifications/copy-composer.ts +160 -30
- package/src/notifications/decision-engine.ts +98 -1
- package/src/runtime/access-request-helper.ts +43 -28
- package/src/runtime/actor-refresh-token-service.ts +309 -0
- package/src/runtime/actor-refresh-token-store.ts +157 -0
- package/src/runtime/actor-token-service.ts +3 -3
- package/src/runtime/actor-trust-resolver.ts +19 -14
- package/src/runtime/channel-guardian-service.ts +6 -0
- package/src/runtime/gateway-client.ts +239 -0
- package/src/runtime/guardian-context-resolver.ts +6 -2
- package/src/runtime/guardian-reply-router.ts +33 -16
- package/src/runtime/guardian-vellum-migration.ts +29 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/http-types.ts +0 -13
- package/src/runtime/local-actor-identity.ts +19 -13
- package/src/runtime/middleware/actor-token.ts +2 -2
- package/src/runtime/routes/channel-delivery-routes.ts +5 -5
- package/src/runtime/routes/conversation-routes.ts +45 -35
- package/src/runtime/routes/guardian-action-routes.ts +7 -1
- package/src/runtime/routes/guardian-approval-interception.ts +52 -52
- package/src/runtime/routes/guardian-bootstrap-routes.ts +11 -24
- package/src/runtime/routes/guardian-refresh-routes.ts +53 -0
- package/src/runtime/routes/inbound-conversation.ts +7 -7
- package/src/runtime/routes/inbound-message-handler.ts +105 -94
- package/src/runtime/routes/pairing-routes.ts +60 -50
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/types/qrcode.d.ts +10 -0
- package/src/util/logger.ts +10 -0
- package/src/daemon/call-pointer-generators.ts +0 -59
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for caller identity resolution
|
|
2
|
+
* Unit tests for caller identity resolution and pointer message regression
|
|
3
|
+
* in call-domain.ts.
|
|
3
4
|
*
|
|
4
|
-
* Validates
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* - Explicit user_number calls fail clearly when missing/ineligible.
|
|
8
|
-
* - Explicit override rejected when allowPerCallOverride=false.
|
|
5
|
+
* Validates:
|
|
6
|
+
* - Strict implicit-default policy for caller identity.
|
|
7
|
+
* - Pointer messages are written on successful call start and on failure.
|
|
9
8
|
*/
|
|
10
|
-
import { mkdtempSync, realpathSync } from 'node:fs';
|
|
9
|
+
import { mkdtempSync, realpathSync, rmSync } from 'node:fs';
|
|
11
10
|
import { tmpdir } from 'node:os';
|
|
12
11
|
import { join } from 'node:path';
|
|
13
12
|
|
|
14
|
-
import { describe, expect, mock,test } from 'bun:test';
|
|
13
|
+
import { afterAll, beforeEach, describe, expect, mock,test } from 'bun:test';
|
|
15
14
|
|
|
16
15
|
const testDir = realpathSync(mkdtempSync(join(tmpdir(), 'call-domain-test-')));
|
|
17
16
|
|
|
@@ -34,6 +33,9 @@ mock.module('../util/logger.js', () => ({
|
|
|
34
33
|
}),
|
|
35
34
|
}));
|
|
36
35
|
|
|
36
|
+
// Track whether the Twilio provider's initiateCall should succeed or throw
|
|
37
|
+
let twilioInitiateCallBehavior: 'success' | 'error' = 'success';
|
|
38
|
+
|
|
37
39
|
mock.module('../calls/twilio-config.js', () => ({
|
|
38
40
|
getTwilioConfig: (assistantId?: string) => ({
|
|
39
41
|
accountSid: 'AC_test',
|
|
@@ -47,10 +49,13 @@ mock.module('../calls/twilio-config.js', () => ({
|
|
|
47
49
|
mock.module('../calls/twilio-provider.js', () => ({
|
|
48
50
|
TwilioConversationRelayProvider: class {
|
|
49
51
|
async checkCallerIdEligibility(number: string) {
|
|
50
|
-
// Simulate: +15550002222 is eligible, others are not
|
|
51
52
|
if (number === '+15550002222') return { eligible: true };
|
|
52
53
|
return { eligible: false, reason: `${number} is not eligible as a caller ID` };
|
|
53
54
|
}
|
|
55
|
+
async initiateCall() {
|
|
56
|
+
if (twilioInitiateCallBehavior === 'error') throw new Error('Twilio unavailable');
|
|
57
|
+
return { callSid: 'CA_test_123' };
|
|
58
|
+
}
|
|
54
59
|
},
|
|
55
60
|
}));
|
|
56
61
|
|
|
@@ -58,8 +63,91 @@ mock.module('../security/secure-keys.js', () => ({
|
|
|
58
63
|
getSecureKey: () => null,
|
|
59
64
|
}));
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
mock.module('../config/loader.js', () => ({
|
|
67
|
+
loadConfig: () => ({
|
|
68
|
+
calls: {
|
|
69
|
+
enabled: true,
|
|
70
|
+
provider: 'twilio',
|
|
71
|
+
callerIdentity: { allowPerCallOverride: true },
|
|
72
|
+
},
|
|
73
|
+
memory: { enabled: false },
|
|
74
|
+
}),
|
|
75
|
+
getConfig: () => ({
|
|
76
|
+
calls: {
|
|
77
|
+
enabled: true,
|
|
78
|
+
provider: 'twilio',
|
|
79
|
+
callerIdentity: { allowPerCallOverride: true },
|
|
80
|
+
},
|
|
81
|
+
memory: { enabled: false },
|
|
82
|
+
}),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
mock.module('../inbound/platform-callback-registration.js', () => ({
|
|
86
|
+
resolveCallbackUrl: async (fn: () => string) => fn(),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
mock.module('../inbound/public-ingress-urls.js', () => ({
|
|
90
|
+
getTwilioVoiceWebhookUrl: () => 'https://test.example.com/webhooks/twilio/voice/test',
|
|
91
|
+
getTwilioStatusCallbackUrl: () => 'https://test.example.com/webhooks/twilio/status',
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
mock.module('../memory/conversation-title-service.js', () => ({
|
|
95
|
+
queueGenerateConversationTitle: () => {},
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
import { resolveCallerIdentity, startCall } from '../calls/call-domain.js';
|
|
62
99
|
import type { AssistantConfig } from '../config/types.js';
|
|
100
|
+
import { getMessages } from '../memory/conversation-store.js';
|
|
101
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
102
|
+
import { conversations } from '../memory/schema.js';
|
|
103
|
+
|
|
104
|
+
initializeDb();
|
|
105
|
+
|
|
106
|
+
let ensuredConvIds = new Set<string>();
|
|
107
|
+
function ensureConversation(id: string): void {
|
|
108
|
+
if (ensuredConvIds.has(id)) return;
|
|
109
|
+
const db = getDb();
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
db.insert(conversations).values({
|
|
112
|
+
id,
|
|
113
|
+
title: `Test conversation ${id}`,
|
|
114
|
+
createdAt: now,
|
|
115
|
+
updatedAt: now,
|
|
116
|
+
}).run();
|
|
117
|
+
ensuredConvIds.add(id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resetTables(): void {
|
|
121
|
+
const db = getDb();
|
|
122
|
+
db.run('DELETE FROM external_conversation_bindings');
|
|
123
|
+
db.run('DELETE FROM call_sessions');
|
|
124
|
+
db.run('DELETE FROM messages');
|
|
125
|
+
db.run('DELETE FROM conversations');
|
|
126
|
+
ensuredConvIds = new Set();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getLatestAssistantText(conversationId: string): string | null {
|
|
130
|
+
const msgs = getMessages(conversationId).filter((m) => m.role === 'assistant');
|
|
131
|
+
if (msgs.length === 0) return null;
|
|
132
|
+
const latest = msgs[msgs.length - 1];
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(latest.content) as unknown;
|
|
135
|
+
if (Array.isArray(parsed)) {
|
|
136
|
+
return parsed
|
|
137
|
+
.filter((b): b is { type: string; text?: string } => typeof b === 'object' && b != null)
|
|
138
|
+
.filter((b) => b.type === 'text')
|
|
139
|
+
.map((b) => b.text ?? '')
|
|
140
|
+
.join('');
|
|
141
|
+
}
|
|
142
|
+
if (typeof parsed === 'string') return parsed;
|
|
143
|
+
} catch { /* fall through */ }
|
|
144
|
+
return latest.content;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
afterAll(() => {
|
|
148
|
+
resetDb();
|
|
149
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
150
|
+
});
|
|
63
151
|
|
|
64
152
|
function makeConfig(overrides: {
|
|
65
153
|
allowPerCallOverride?: boolean;
|
|
@@ -172,3 +260,53 @@ describe('resolveCallerIdentity — strict implicit-default policy', () => {
|
|
|
172
260
|
}
|
|
173
261
|
});
|
|
174
262
|
});
|
|
263
|
+
|
|
264
|
+
// ── Pointer message regression tests ──────────────────────────────
|
|
265
|
+
|
|
266
|
+
describe('startCall — pointer message regression', () => {
|
|
267
|
+
beforeEach(() => {
|
|
268
|
+
resetTables();
|
|
269
|
+
twilioInitiateCallBehavior = 'success';
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('successful call writes a started pointer to the initiating conversation', async () => {
|
|
273
|
+
const convId = 'conv-domain-ptr-start';
|
|
274
|
+
ensureConversation(convId);
|
|
275
|
+
|
|
276
|
+
const result = await startCall({
|
|
277
|
+
phoneNumber: '+15559876543',
|
|
278
|
+
task: 'Test call',
|
|
279
|
+
conversationId: convId,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(result.ok).toBe(true);
|
|
283
|
+
// Allow async pointer write to flush
|
|
284
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
285
|
+
|
|
286
|
+
const text = getLatestAssistantText(convId);
|
|
287
|
+
expect(text).not.toBeNull();
|
|
288
|
+
expect(text!).toContain('+15559876543');
|
|
289
|
+
expect(text!).toContain('started');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('failed call writes a failed pointer to the initiating conversation', async () => {
|
|
293
|
+
const convId = 'conv-domain-ptr-fail';
|
|
294
|
+
ensureConversation(convId);
|
|
295
|
+
twilioInitiateCallBehavior = 'error';
|
|
296
|
+
|
|
297
|
+
const result = await startCall({
|
|
298
|
+
phoneNumber: '+15559876543',
|
|
299
|
+
task: 'Test call',
|
|
300
|
+
conversationId: convId,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(result.ok).toBe(false);
|
|
304
|
+
// Allow async pointer write to flush
|
|
305
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
306
|
+
|
|
307
|
+
const text = getLatestAssistantText(convId);
|
|
308
|
+
expect(text).not.toBeNull();
|
|
309
|
+
expect(text!).toContain('+15559876543');
|
|
310
|
+
expect(text!).toContain('failed');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -9,11 +9,9 @@ mock.module('../util/logger.js', () => ({
|
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
buildPointerInstruction,
|
|
13
13
|
type CallPointerMessageContext,
|
|
14
|
-
composeCallPointerMessageGenerative,
|
|
15
14
|
getPointerFallbackMessage,
|
|
16
|
-
includesRequiredFacts,
|
|
17
15
|
} from '../calls/call-pointer-message-composer.js';
|
|
18
16
|
|
|
19
17
|
// ---------------------------------------------------------------------------
|
|
@@ -105,67 +103,59 @@ describe('getPointerFallbackMessage', () => {
|
|
|
105
103
|
});
|
|
106
104
|
|
|
107
105
|
// ---------------------------------------------------------------------------
|
|
108
|
-
//
|
|
106
|
+
// Daemon instruction builder
|
|
109
107
|
// ---------------------------------------------------------------------------
|
|
110
108
|
|
|
111
|
-
describe('
|
|
112
|
-
test('
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
describe('buildPointerInstruction', () => {
|
|
110
|
+
test('includes event tag, scenario, and phone number', () => {
|
|
111
|
+
const ctx: CallPointerMessageContext = { scenario: 'started', phoneNumber: '+15551234567' };
|
|
112
|
+
const instruction = buildPointerInstruction(ctx);
|
|
113
|
+
expect(instruction).toContain('[CALL_STATUS_EVENT]');
|
|
114
|
+
expect(instruction).toContain('Event: started');
|
|
115
|
+
expect(instruction).toContain('Phone number: +15551234567');
|
|
115
116
|
});
|
|
116
117
|
|
|
117
|
-
test('
|
|
118
|
-
|
|
118
|
+
test('includes duration when provided', () => {
|
|
119
|
+
const ctx: CallPointerMessageContext = { scenario: 'completed', phoneNumber: '+15559876543', duration: '3m' };
|
|
120
|
+
const instruction = buildPointerInstruction(ctx);
|
|
121
|
+
expect(instruction).toContain('Duration: 3m');
|
|
119
122
|
});
|
|
120
123
|
|
|
121
|
-
test('
|
|
122
|
-
|
|
124
|
+
test('includes reason when provided', () => {
|
|
125
|
+
const ctx: CallPointerMessageContext = { scenario: 'failed', phoneNumber: '+15559876543', reason: 'no answer' };
|
|
126
|
+
const instruction = buildPointerInstruction(ctx);
|
|
127
|
+
expect(instruction).toContain('Reason: no answer');
|
|
123
128
|
});
|
|
124
|
-
});
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
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');
|
|
130
|
+
test('includes verification code when provided', () => {
|
|
131
|
+
const ctx: CallPointerMessageContext = { scenario: 'started', phoneNumber: '+15551234567', verificationCode: '42' };
|
|
132
|
+
const instruction = buildPointerInstruction(ctx);
|
|
133
|
+
expect(instruction).toContain('Verification code: 42');
|
|
136
134
|
});
|
|
137
135
|
|
|
138
|
-
test('includes
|
|
139
|
-
const ctx: CallPointerMessageContext = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
test('includes channel when provided', () => {
|
|
137
|
+
const ctx: CallPointerMessageContext = {
|
|
138
|
+
scenario: 'guardian_verification_succeeded',
|
|
139
|
+
phoneNumber: '+15559876543',
|
|
140
|
+
channel: 'sms',
|
|
141
|
+
};
|
|
142
|
+
const instruction = buildPointerInstruction(ctx);
|
|
143
|
+
expect(instruction).toContain('Channel: sms');
|
|
144
144
|
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Generative composition (test env falls back to deterministic)
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
145
|
|
|
151
|
-
|
|
152
|
-
test('returns fallback in test environment regardless of generator', async () => {
|
|
153
|
-
const generator = async () => 'LLM-generated copy';
|
|
146
|
+
test('omits optional fields when not provided', () => {
|
|
154
147
|
const ctx: CallPointerMessageContext = { scenario: 'started', phoneNumber: '+15551234567' };
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
expect(
|
|
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');
|
|
148
|
+
const instruction = buildPointerInstruction(ctx);
|
|
149
|
+
expect(instruction).not.toContain('Duration:');
|
|
150
|
+
expect(instruction).not.toContain('Reason:');
|
|
151
|
+
expect(instruction).not.toContain('Verification code:');
|
|
152
|
+
expect(instruction).not.toContain('Channel:');
|
|
164
153
|
});
|
|
165
154
|
|
|
166
|
-
test('
|
|
155
|
+
test('ends with generation instructions', () => {
|
|
167
156
|
const ctx: CallPointerMessageContext = { scenario: 'completed', phoneNumber: '+15559876543' };
|
|
168
|
-
const
|
|
169
|
-
expect(
|
|
157
|
+
const instruction = buildPointerInstruction(ctx);
|
|
158
|
+
expect(instruction).toContain('Write a brief');
|
|
159
|
+
expect(instruction).toContain('Preserve all factual details');
|
|
170
160
|
});
|
|
171
161
|
});
|
|
@@ -25,8 +25,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
25
25
|
}),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
-
import { addPointerMessage, formatDuration,
|
|
29
|
-
import { getMessages } from '../memory/conversation-store.js';
|
|
28
|
+
import { addPointerMessage, formatDuration, resetPointerMessageProcessor, setPointerMessageProcessor } from '../calls/call-pointer-messages.js';
|
|
29
|
+
import { addMessage, 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
|
|
|
@@ -95,7 +95,7 @@ describe('addPointerMessage', () => {
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
afterEach(() => {
|
|
98
|
-
|
|
98
|
+
resetPointerMessageProcessor();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
afterAll(() => {
|
|
@@ -192,87 +192,149 @@ describe('addPointerMessage', () => {
|
|
|
192
192
|
expect(text).toContain('failed: Max attempts exceeded');
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
// Trust-aware tests
|
|
196
|
-
// short-circuits to fallback), so these validate the trust gating path
|
|
197
|
-
// while still receiving deterministic text.
|
|
195
|
+
// Trust-aware tests
|
|
198
196
|
|
|
199
|
-
test('untrusted audience uses deterministic fallback even with
|
|
197
|
+
test('untrusted audience uses deterministic fallback even with processor set', () => {
|
|
200
198
|
const convId = 'conv-ptr-untrusted';
|
|
201
|
-
// standard threadType + no origin channel = untrusted
|
|
202
199
|
ensureConversation(convId, { threadType: 'standard' });
|
|
203
200
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return 'generated text';
|
|
201
|
+
const processorCalled = { value: false };
|
|
202
|
+
setPointerMessageProcessor(async () => {
|
|
203
|
+
processorCalled.value = true;
|
|
208
204
|
});
|
|
209
205
|
|
|
210
206
|
addPointerMessage(convId, 'started', '+15551234567');
|
|
211
207
|
const text = getLatestAssistantText(convId);
|
|
212
|
-
// In test env, deterministic fallback is always used regardless of trust
|
|
213
208
|
expect(text).toContain('Call to +15551234567 started');
|
|
209
|
+
// processor not called because standard threadType + no origin = untrusted
|
|
210
|
+
expect(processorCalled.value).toBe(false);
|
|
214
211
|
});
|
|
215
212
|
|
|
216
|
-
test('explicit untrusted audience mode skips
|
|
213
|
+
test('explicit untrusted audience mode skips processor', () => {
|
|
217
214
|
const convId = 'conv-ptr-explicit-untrusted';
|
|
218
215
|
ensureConversation(convId, { threadType: 'private' });
|
|
219
216
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return 'generated text';
|
|
217
|
+
const processorCalled = { value: false };
|
|
218
|
+
setPointerMessageProcessor(async () => {
|
|
219
|
+
processorCalled.value = true;
|
|
224
220
|
});
|
|
225
221
|
|
|
226
222
|
addPointerMessage(convId, 'started', '+15551234567', undefined, 'untrusted');
|
|
227
223
|
const text = getLatestAssistantText(convId);
|
|
228
224
|
expect(text).toContain('Call to +15551234567 started');
|
|
229
|
-
|
|
230
|
-
expect(generatorCalled.value).toBe(false);
|
|
225
|
+
expect(processorCalled.value).toBe(false);
|
|
231
226
|
});
|
|
232
227
|
|
|
233
|
-
test('
|
|
234
|
-
const convId = 'conv-ptr-
|
|
228
|
+
test('trusted audience routes through daemon processor with required facts', async () => {
|
|
229
|
+
const convId = 'conv-ptr-trusted';
|
|
235
230
|
ensureConversation(convId, { threadType: 'private' });
|
|
236
231
|
|
|
237
|
-
|
|
232
|
+
let capturedInstruction = '';
|
|
233
|
+
let capturedFacts: string[] = [];
|
|
234
|
+
setPointerMessageProcessor(async (_convId, instruction, requiredFacts) => {
|
|
235
|
+
capturedInstruction = instruction;
|
|
236
|
+
capturedFacts = requiredFacts ?? [];
|
|
237
|
+
});
|
|
238
238
|
|
|
239
239
|
await addPointerMessage(convId, 'completed', '+15559876543', { duration: '1m' });
|
|
240
|
+
// Processor was called with a structured instruction
|
|
241
|
+
expect(capturedInstruction).toContain('[CALL_STATUS_EVENT]');
|
|
242
|
+
expect(capturedInstruction).toContain('+15559876543');
|
|
243
|
+
expect(capturedInstruction).toContain('completed');
|
|
244
|
+
expect(capturedInstruction).toContain('1m');
|
|
245
|
+
// Required facts include phone number, duration, and outcome keyword
|
|
246
|
+
expect(capturedFacts).toContain('+15559876543');
|
|
247
|
+
expect(capturedFacts).toContain('1m');
|
|
248
|
+
expect(capturedFacts).toContain('completed');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('trusted audience falls back to deterministic on processor failure', async () => {
|
|
252
|
+
const convId = 'conv-ptr-processor-fail';
|
|
253
|
+
ensureConversation(convId, { threadType: 'private' });
|
|
254
|
+
|
|
255
|
+
setPointerMessageProcessor(async () => {
|
|
256
|
+
throw new Error('Daemon unavailable');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await addPointerMessage(convId, 'failed', '+15559876543', { reason: 'busy' });
|
|
260
|
+
// Falls back to deterministic — written directly to conversation store
|
|
240
261
|
const text = getLatestAssistantText(convId);
|
|
241
|
-
|
|
242
|
-
expect(text).toContain('Call to +15559876543 completed (1m)');
|
|
262
|
+
expect(text).toContain('failed: busy');
|
|
243
263
|
});
|
|
244
264
|
|
|
245
265
|
test('vellum origin channel is detected as trusted audience', async () => {
|
|
246
266
|
const convId = 'conv-ptr-vellum';
|
|
247
267
|
ensureConversation(convId, { originChannel: 'vellum' });
|
|
248
268
|
|
|
249
|
-
|
|
269
|
+
let processorCalled = false;
|
|
270
|
+
setPointerMessageProcessor(async () => {
|
|
271
|
+
processorCalled = true;
|
|
272
|
+
});
|
|
250
273
|
|
|
251
274
|
await addPointerMessage(convId, 'failed', '+15559876543', { reason: 'busy' });
|
|
252
|
-
|
|
253
|
-
expect(text).toContain('failed: busy');
|
|
275
|
+
expect(processorCalled).toBe(true);
|
|
254
276
|
});
|
|
255
277
|
|
|
256
278
|
test('missing conversation defaults to untrusted', () => {
|
|
257
|
-
const
|
|
258
|
-
|
|
279
|
+
const convId = 'conv-ptr-no-signals';
|
|
280
|
+
ensureConversation(convId);
|
|
281
|
+
|
|
282
|
+
const processorCalled = { value: false };
|
|
283
|
+
setPointerMessageProcessor(async () => {
|
|
284
|
+
processorCalled.value = true;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
addPointerMessage(convId, 'started', '+15551234567');
|
|
288
|
+
const text = getLatestAssistantText(convId);
|
|
289
|
+
expect(text).toContain('Call to +15551234567 started');
|
|
290
|
+
expect(processorCalled.value).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Provenance trust class tests
|
|
294
|
+
|
|
295
|
+
test('guardian provenance trust class is detected as trusted audience', async () => {
|
|
296
|
+
const convId = 'conv-ptr-guardian-provenance';
|
|
297
|
+
ensureConversation(convId, { threadType: 'standard' });
|
|
298
|
+
// Add a user message with guardian provenance metadata
|
|
299
|
+
await addMessage(convId, 'user', 'hello', { provenanceTrustClass: 'guardian' });
|
|
300
|
+
|
|
301
|
+
let processorCalled = false;
|
|
302
|
+
setPointerMessageProcessor(async () => {
|
|
303
|
+
processorCalled = true;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await addPointerMessage(convId, 'completed', '+15559876543');
|
|
307
|
+
expect(processorCalled).toBe(true);
|
|
308
|
+
});
|
|
259
309
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
310
|
+
test('trusted_contact provenance trust class is detected as trusted audience', async () => {
|
|
311
|
+
const convId = 'conv-ptr-tc-provenance';
|
|
312
|
+
ensureConversation(convId, { threadType: 'standard' });
|
|
313
|
+
// Add a user message with trusted_contact provenance metadata
|
|
314
|
+
await addMessage(convId, 'user', 'hello', { provenanceTrustClass: 'trusted_contact' });
|
|
315
|
+
|
|
316
|
+
let processorCalled = false;
|
|
317
|
+
setPointerMessageProcessor(async () => {
|
|
318
|
+
processorCalled = true;
|
|
264
319
|
});
|
|
265
320
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
321
|
+
await addPointerMessage(convId, 'completed', '+15559876543');
|
|
322
|
+
expect(processorCalled).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('unknown provenance trust class does not grant trusted audience', () => {
|
|
326
|
+
const convId = 'conv-ptr-unknown-provenance';
|
|
327
|
+
ensureConversation(convId, { threadType: 'standard' });
|
|
328
|
+
addMessage(convId, 'user', 'hello', { provenanceTrustClass: 'unknown' });
|
|
271
329
|
|
|
272
|
-
|
|
273
|
-
|
|
330
|
+
const processorCalled = { value: false };
|
|
331
|
+
setPointerMessageProcessor(async () => {
|
|
332
|
+
processorCalled.value = true;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
addPointerMessage(convId, 'started', '+15551234567');
|
|
336
|
+
const text = getLatestAssistantText(convId);
|
|
274
337
|
expect(text).toContain('Call to +15551234567 started');
|
|
275
|
-
|
|
276
|
-
expect(generatorCalled.value).toBe(false);
|
|
338
|
+
expect(processorCalled.value).toBe(false);
|
|
277
339
|
});
|
|
278
340
|
});
|