@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
|
@@ -31,16 +31,13 @@ mock.module('../util/logger.js', () => ({
|
|
|
31
31
|
}),
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
let semanticSearchDelayMs = 0;
|
|
34
|
+
// Counter for semantic search invocations — used to verify early termination
|
|
35
|
+
// skips the call entirely rather than relying on flaky wall-clock comparisons.
|
|
36
|
+
let semanticSearchCallCount = 0;
|
|
38
37
|
|
|
39
38
|
mock.module('../memory/search/semantic.js', () => ({
|
|
40
39
|
semanticSearch: async () => {
|
|
41
|
-
|
|
42
|
-
await Bun.sleep(semanticSearchDelayMs);
|
|
43
|
-
}
|
|
40
|
+
semanticSearchCallCount++;
|
|
44
41
|
return [];
|
|
45
42
|
},
|
|
46
43
|
isQdrantConnectionError: () => false,
|
|
@@ -305,15 +302,11 @@ describe('Memory retrieval benchmark', () => {
|
|
|
305
302
|
expect(recall.selectedCount).toBeGreaterThan(0);
|
|
306
303
|
});
|
|
307
304
|
|
|
308
|
-
test('early termination
|
|
309
|
-
const conversationId = 'conv-bench-et-
|
|
305
|
+
test('early termination skips semantic search entirely', async () => {
|
|
306
|
+
const conversationId = 'conv-bench-et-skip';
|
|
310
307
|
const now = 1_700_500_000_000;
|
|
311
308
|
seedMemoryItems(conversationId, 500, now);
|
|
312
309
|
|
|
313
|
-
// Simulate the Qdrant network round-trip that ET is designed to skip.
|
|
314
|
-
// Use 100ms to dominate over variable CPU-bound work on slower hosts.
|
|
315
|
-
semanticSearchDelayMs = 100;
|
|
316
|
-
|
|
317
310
|
const query = 'What do we know about topic-5 and keyword-3?';
|
|
318
311
|
|
|
319
312
|
const etConfig: AssistantConfig = {
|
|
@@ -363,40 +356,22 @@ describe('Memory retrieval benchmark', () => {
|
|
|
363
356
|
},
|
|
364
357
|
};
|
|
365
358
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const baselineRecall = await buildMemoryRecall(query, conversationId, noEtConfig);
|
|
383
|
-
baselineTimes.push(performance.now() - t1);
|
|
384
|
-
expect(baselineRecall.earlyTerminated).toBe(false);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
etTimes.sort((a, b) => a - b);
|
|
388
|
-
baselineTimes.sort((a, b) => a - b);
|
|
389
|
-
const medianEt = etTimes[Math.floor(iterations / 2)];
|
|
390
|
-
const medianBaseline = baselineTimes[Math.floor(iterations / 2)];
|
|
391
|
-
|
|
392
|
-
// ET skips the mocked network delay, so it should be measurably faster.
|
|
393
|
-
// Use a 15% threshold to tolerate slower CI hosts where CPU-bound work
|
|
394
|
-
// takes longer relative to the fixed mock delay.
|
|
395
|
-
const speedup = 1 - medianEt / medianBaseline;
|
|
396
|
-
expect(speedup).toBeGreaterThanOrEqual(0.15);
|
|
397
|
-
} finally {
|
|
398
|
-
semanticSearchDelayMs = 0;
|
|
399
|
-
}
|
|
359
|
+
// Run with ET enabled — semantic search should be skipped
|
|
360
|
+
semanticSearchCallCount = 0;
|
|
361
|
+
const etRecall = await buildMemoryRecall(query, conversationId, etConfig);
|
|
362
|
+
const etCalls = semanticSearchCallCount;
|
|
363
|
+
|
|
364
|
+
expect(etRecall.earlyTerminated).toBe(true);
|
|
365
|
+
expect(etRecall.semanticHits).toBe(0);
|
|
366
|
+
expect(etCalls).toBe(0);
|
|
367
|
+
|
|
368
|
+
// Run without ET — semantic search should be invoked
|
|
369
|
+
semanticSearchCallCount = 0;
|
|
370
|
+
const baselineRecall = await buildMemoryRecall(query, conversationId, noEtConfig);
|
|
371
|
+
const baselineCalls = semanticSearchCallCount;
|
|
372
|
+
|
|
373
|
+
expect(baselineRecall.earlyTerminated).toBe(false);
|
|
374
|
+
expect(baselineCalls).toBeGreaterThan(0);
|
|
400
375
|
});
|
|
401
376
|
|
|
402
377
|
test('recall.latencyMs tracks wall-clock within 50% tolerance', async () => {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard test: `isTrusted` must not appear in production code.
|
|
3
|
+
*
|
|
4
|
+
* The authorization model was migrated from a boolean `isTrusted` flag to
|
|
5
|
+
* principal-based authorization (`guardianPrincipalId` matching). This guard
|
|
6
|
+
* ensures the legacy pattern is never reintroduced in production source files.
|
|
7
|
+
*
|
|
8
|
+
* The invariant: `actor.guardianPrincipalId === request.guardianPrincipalId`
|
|
9
|
+
* (with cross-channel fallback via the vellum canonical principal).
|
|
10
|
+
*
|
|
11
|
+
* Allowed exceptions:
|
|
12
|
+
* - Variable names like `isTrustedActor` or `isTrustedContact` that refer
|
|
13
|
+
* to trust-class checks (e.g. `trustClass === 'guardian'`), NOT to a
|
|
14
|
+
* boolean `isTrusted` property on ActorContext.
|
|
15
|
+
* - Test files (__tests__/) — may reference `isTrusted` in test descriptions
|
|
16
|
+
* or comments about the migration.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { describe, expect, test } from 'bun:test';
|
|
23
|
+
|
|
24
|
+
const repoRoot = resolve(__dirname, '..', '..', '..');
|
|
25
|
+
|
|
26
|
+
describe('isTrusted guard', () => {
|
|
27
|
+
test('isTrusted property must not exist in production ActorContext usage', () => {
|
|
28
|
+
// Search for `isTrusted` used as a property (e.g., `.isTrusted`, `isTrusted:`,
|
|
29
|
+
// `isTrusted =`) in production TypeScript files, excluding tests, node_modules,
|
|
30
|
+
// and the allowed trust-class variable pattern.
|
|
31
|
+
const raw = execSync(
|
|
32
|
+
[
|
|
33
|
+
'grep -rn "isTrusted" assistant/src/ --include="*.ts"',
|
|
34
|
+
'grep -v "__tests__"',
|
|
35
|
+
'grep -v "node_modules"',
|
|
36
|
+
].join(' | ') + ' || true',
|
|
37
|
+
{ encoding: 'utf-8', cwd: repoRoot },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Filter in JS: strip allowed token names from each line, then check if
|
|
41
|
+
// `isTrusted` still appears. This avoids the grep -v approach which could
|
|
42
|
+
// mask forbidden usage on lines that also contain allowed tokens.
|
|
43
|
+
const ALLOWED_TOKENS = ['isTrustedActor', 'isTrustedContact', 'isTrustedTrustClass'];
|
|
44
|
+
const offending = raw
|
|
45
|
+
.trim()
|
|
46
|
+
.split('\n')
|
|
47
|
+
.filter((line) => {
|
|
48
|
+
if (!line) return false;
|
|
49
|
+
let stripped = line;
|
|
50
|
+
for (const token of ALLOWED_TOKENS) {
|
|
51
|
+
stripped = stripped.replaceAll(token, '');
|
|
52
|
+
}
|
|
53
|
+
return stripped.includes('isTrusted');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (offending.length > 0) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Found `isTrusted` references in production code. Authorization must use ' +
|
|
59
|
+
'`guardianPrincipalId` matching instead. Offending lines:\n' +
|
|
60
|
+
offending.join('\n'),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('ActorContext interface must not declare isTrusted field', () => {
|
|
66
|
+
// Verify the ActorContext type definition does not include isTrusted
|
|
67
|
+
const result = execSync(
|
|
68
|
+
[
|
|
69
|
+
'grep -n "isTrusted" assistant/src/approvals/guardian-request-resolvers.ts',
|
|
70
|
+
'true',
|
|
71
|
+
].join(' || '),
|
|
72
|
+
{ encoding: 'utf-8', cwd: repoRoot },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(result.trim()).toBe('');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -138,12 +138,12 @@ function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
|
138
138
|
const body: Record<string, unknown> = {
|
|
139
139
|
sourceChannel: 'telegram',
|
|
140
140
|
interface: 'telegram',
|
|
141
|
-
|
|
141
|
+
conversationExternalId: 'chat-123',
|
|
142
142
|
externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
143
143
|
content: 'Hello, can I use this assistant?',
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
actorExternalId: 'user-unknown-456',
|
|
145
|
+
actorDisplayName: 'Alice Unknown',
|
|
146
|
+
actorUsername: 'alice_unknown',
|
|
147
147
|
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
148
148
|
...overrides,
|
|
149
149
|
};
|
|
@@ -188,6 +188,7 @@ describe('non-member access request notification', () => {
|
|
|
188
188
|
channel: 'telegram',
|
|
189
189
|
guardianExternalUserId: 'guardian-user-789',
|
|
190
190
|
guardianDeliveryChatId: 'guardian-chat-789',
|
|
191
|
+
guardianPrincipalId: 'test-principal-id',
|
|
191
192
|
});
|
|
192
193
|
|
|
193
194
|
const req = buildInboundRequest();
|
|
@@ -206,8 +207,8 @@ describe('non-member access request notification', () => {
|
|
|
206
207
|
expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
|
|
207
208
|
expect(emitSignalCalls[0].sourceChannel).toBe('telegram');
|
|
208
209
|
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
209
|
-
expect(payload.
|
|
210
|
-
expect(payload.
|
|
210
|
+
expect(payload.actorExternalId).toBe('user-unknown-456');
|
|
211
|
+
expect(payload.actorDisplayName).toBe('Alice Unknown');
|
|
211
212
|
|
|
212
213
|
// A canonical access request was created
|
|
213
214
|
const pending = listCanonicalGuardianRequests({
|
|
@@ -229,6 +230,7 @@ describe('non-member access request notification', () => {
|
|
|
229
230
|
channel: 'telegram',
|
|
230
231
|
guardianExternalUserId: 'guardian-user-789',
|
|
231
232
|
guardianDeliveryChatId: 'guardian-chat-789',
|
|
233
|
+
guardianPrincipalId: 'test-principal-id',
|
|
232
234
|
});
|
|
233
235
|
|
|
234
236
|
// First message
|
|
@@ -258,9 +260,9 @@ describe('non-member access request notification', () => {
|
|
|
258
260
|
expect(pending.length).toBe(1);
|
|
259
261
|
});
|
|
260
262
|
|
|
261
|
-
test('access request is created
|
|
262
|
-
// No guardian binding on any channel —
|
|
263
|
-
//
|
|
263
|
+
test('access request is created with self-healed principal even without same-channel guardian binding', async () => {
|
|
264
|
+
// No guardian binding on any channel — self-heal creates a vellum binding
|
|
265
|
+
// so the access_request (now decisionable) has a guardianPrincipalId.
|
|
264
266
|
const req = buildInboundRequest();
|
|
265
267
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
266
268
|
const json = await resp.json() as Record<string, unknown>;
|
|
@@ -276,7 +278,7 @@ describe('non-member access request notification', () => {
|
|
|
276
278
|
expect(emitSignalCalls.length).toBe(1);
|
|
277
279
|
expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
|
|
278
280
|
|
|
279
|
-
// Canonical request was created with
|
|
281
|
+
// Canonical request was created with a self-healed principal
|
|
280
282
|
const pending = listCanonicalGuardianRequests({
|
|
281
283
|
status: 'pending',
|
|
282
284
|
requesterExternalUserId: 'user-unknown-456',
|
|
@@ -284,7 +286,9 @@ describe('non-member access request notification', () => {
|
|
|
284
286
|
kind: 'access_request',
|
|
285
287
|
});
|
|
286
288
|
expect(pending.length).toBe(1);
|
|
287
|
-
|
|
289
|
+
// Self-heal bootstraps a vellum binding — guardianExternalUserId is now set
|
|
290
|
+
expect(pending[0].guardianExternalUserId).toBeDefined();
|
|
291
|
+
expect(pending[0].guardianPrincipalId).toBeDefined();
|
|
288
292
|
});
|
|
289
293
|
|
|
290
294
|
test('cross-channel fallback: SMS guardian binding resolves for Telegram access request', async () => {
|
|
@@ -294,6 +298,7 @@ describe('non-member access request notification', () => {
|
|
|
294
298
|
channel: 'sms',
|
|
295
299
|
guardianExternalUserId: 'guardian-sms-user',
|
|
296
300
|
guardianDeliveryChatId: 'guardian-sms-chat',
|
|
301
|
+
guardianPrincipalId: 'test-principal-id',
|
|
297
302
|
});
|
|
298
303
|
|
|
299
304
|
const req = buildInboundRequest();
|
|
@@ -319,21 +324,21 @@ describe('non-member access request notification', () => {
|
|
|
319
324
|
expect(pending[0].guardianExternalUserId).toBe('guardian-sms-user');
|
|
320
325
|
});
|
|
321
326
|
|
|
322
|
-
test('no notification when
|
|
327
|
+
test('no notification when actorExternalId is absent', async () => {
|
|
323
328
|
createBinding({
|
|
324
329
|
assistantId: 'self',
|
|
325
330
|
channel: 'telegram',
|
|
326
331
|
guardianExternalUserId: 'guardian-user-789',
|
|
327
332
|
guardianDeliveryChatId: 'guardian-chat-789',
|
|
333
|
+
guardianPrincipalId: 'test-principal-id',
|
|
328
334
|
});
|
|
329
335
|
|
|
330
|
-
// Message without
|
|
331
|
-
// The ACL check requires senderExternalUserId to look up members,
|
|
332
|
-
// so without it the non-member gate is bypassed entirely.
|
|
336
|
+
// Message without actorExternalId — the handler returns BAD_REQUEST.
|
|
333
337
|
const req = buildInboundRequest({
|
|
334
|
-
|
|
338
|
+
actorExternalId: undefined,
|
|
335
339
|
});
|
|
336
|
-
await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
340
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
341
|
+
expect(resp.status).toBe(400);
|
|
337
342
|
|
|
338
343
|
// No access request notification should fire (no identity to notify about)
|
|
339
344
|
expect(emitSignalCalls.length).toBe(0);
|
|
@@ -345,12 +350,12 @@ describe('access-request-helper unit tests', () => {
|
|
|
345
350
|
resetState();
|
|
346
351
|
});
|
|
347
352
|
|
|
348
|
-
test('notifyGuardianOfAccessRequest returns no_sender_id when
|
|
353
|
+
test('notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent', () => {
|
|
349
354
|
const result = notifyGuardianOfAccessRequest({
|
|
350
355
|
canonicalAssistantId: 'self',
|
|
351
356
|
sourceChannel: 'telegram',
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
conversationExternalId: 'chat-123',
|
|
358
|
+
actorExternalId: undefined,
|
|
354
359
|
});
|
|
355
360
|
|
|
356
361
|
expect(result.notified).toBe(false);
|
|
@@ -363,13 +368,13 @@ describe('access-request-helper unit tests', () => {
|
|
|
363
368
|
expect(pending.length).toBe(0);
|
|
364
369
|
});
|
|
365
370
|
|
|
366
|
-
test('notifyGuardianOfAccessRequest creates request with
|
|
371
|
+
test('notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists', () => {
|
|
367
372
|
const result = notifyGuardianOfAccessRequest({
|
|
368
373
|
canonicalAssistantId: 'self',
|
|
369
374
|
sourceChannel: 'telegram',
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
375
|
+
conversationExternalId: 'chat-123',
|
|
376
|
+
actorExternalId: 'unknown-user',
|
|
377
|
+
actorDisplayName: 'Bob',
|
|
373
378
|
});
|
|
374
379
|
|
|
375
380
|
expect(result.notified).toBe(true);
|
|
@@ -383,7 +388,9 @@ describe('access-request-helper unit tests', () => {
|
|
|
383
388
|
kind: 'access_request',
|
|
384
389
|
});
|
|
385
390
|
expect(pending.length).toBe(1);
|
|
386
|
-
|
|
391
|
+
// Self-heal bootstraps a vellum binding
|
|
392
|
+
expect(pending[0].guardianExternalUserId).toBeDefined();
|
|
393
|
+
expect(pending[0].guardianPrincipalId).toBeDefined();
|
|
387
394
|
|
|
388
395
|
// Signal was emitted
|
|
389
396
|
expect(emitSignalCalls.length).toBe(1);
|
|
@@ -396,13 +403,14 @@ describe('access-request-helper unit tests', () => {
|
|
|
396
403
|
channel: 'sms',
|
|
397
404
|
guardianExternalUserId: 'guardian-sms',
|
|
398
405
|
guardianDeliveryChatId: 'sms-chat',
|
|
406
|
+
guardianPrincipalId: 'test-principal-id',
|
|
399
407
|
});
|
|
400
408
|
|
|
401
409
|
const result = notifyGuardianOfAccessRequest({
|
|
402
410
|
canonicalAssistantId: 'self',
|
|
403
411
|
sourceChannel: 'telegram',
|
|
404
|
-
|
|
405
|
-
|
|
412
|
+
conversationExternalId: 'tg-chat',
|
|
413
|
+
actorExternalId: 'unknown-tg-user',
|
|
406
414
|
});
|
|
407
415
|
|
|
408
416
|
expect(result.notified).toBe(true);
|
|
@@ -427,19 +435,21 @@ describe('access-request-helper unit tests', () => {
|
|
|
427
435
|
channel: 'telegram',
|
|
428
436
|
guardianExternalUserId: 'guardian-tg',
|
|
429
437
|
guardianDeliveryChatId: 'tg-chat',
|
|
438
|
+
guardianPrincipalId: 'test-principal-tg',
|
|
430
439
|
});
|
|
431
440
|
createBinding({
|
|
432
441
|
assistantId: 'self',
|
|
433
442
|
channel: 'sms',
|
|
434
443
|
guardianExternalUserId: 'guardian-sms',
|
|
435
444
|
guardianDeliveryChatId: 'sms-chat',
|
|
445
|
+
guardianPrincipalId: 'test-principal-sms',
|
|
436
446
|
});
|
|
437
447
|
|
|
438
448
|
const result = notifyGuardianOfAccessRequest({
|
|
439
449
|
canonicalAssistantId: 'self',
|
|
440
450
|
sourceChannel: 'telegram',
|
|
441
|
-
|
|
442
|
-
|
|
451
|
+
conversationExternalId: 'chat-123',
|
|
452
|
+
actorExternalId: 'unknown-user',
|
|
443
453
|
});
|
|
444
454
|
|
|
445
455
|
expect(result.notified).toBe(true);
|
|
@@ -457,13 +467,13 @@ describe('access-request-helper unit tests', () => {
|
|
|
457
467
|
expect(payload.guardianBindingChannel).toBe('telegram');
|
|
458
468
|
});
|
|
459
469
|
|
|
460
|
-
test('notifyGuardianOfAccessRequest for voice channel includes
|
|
470
|
+
test('notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload', () => {
|
|
461
471
|
const result = notifyGuardianOfAccessRequest({
|
|
462
472
|
canonicalAssistantId: 'self',
|
|
463
473
|
sourceChannel: 'voice',
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
474
|
+
conversationExternalId: '+15559998888',
|
|
475
|
+
actorExternalId: '+15559998888',
|
|
476
|
+
actorDisplayName: 'Alice Caller',
|
|
467
477
|
});
|
|
468
478
|
|
|
469
479
|
expect(result.notified).toBe(true);
|
|
@@ -471,8 +481,8 @@ describe('access-request-helper unit tests', () => {
|
|
|
471
481
|
|
|
472
482
|
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
473
483
|
expect(payload.sourceChannel).toBe('voice');
|
|
474
|
-
expect(payload.
|
|
475
|
-
expect(payload.
|
|
484
|
+
expect(payload.actorDisplayName).toBe('Alice Caller');
|
|
485
|
+
expect(payload.actorExternalId).toBe('+15559998888');
|
|
476
486
|
expect(payload.senderIdentifier).toBe('Alice Caller');
|
|
477
487
|
|
|
478
488
|
// Canonical request should exist
|
|
@@ -489,9 +499,9 @@ describe('access-request-helper unit tests', () => {
|
|
|
489
499
|
const result = notifyGuardianOfAccessRequest({
|
|
490
500
|
canonicalAssistantId: 'self',
|
|
491
501
|
sourceChannel: 'telegram',
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
502
|
+
conversationExternalId: 'chat-123',
|
|
503
|
+
actorExternalId: 'unknown-user',
|
|
504
|
+
actorDisplayName: 'Test User',
|
|
495
505
|
});
|
|
496
506
|
|
|
497
507
|
expect(result.notified).toBe(true);
|
|
@@ -507,9 +517,9 @@ describe('access-request-helper unit tests', () => {
|
|
|
507
517
|
const result = notifyGuardianOfAccessRequest({
|
|
508
518
|
canonicalAssistantId: 'self',
|
|
509
519
|
sourceChannel: 'telegram',
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
520
|
+
conversationExternalId: 'chat-123',
|
|
521
|
+
actorExternalId: 'revoked-user',
|
|
522
|
+
actorDisplayName: 'Revoked User',
|
|
513
523
|
previousMemberStatus: 'revoked',
|
|
514
524
|
});
|
|
515
525
|
|
|
@@ -544,9 +554,9 @@ describe('access-request-helper unit tests', () => {
|
|
|
544
554
|
const result = notifyGuardianOfAccessRequest({
|
|
545
555
|
canonicalAssistantId: 'self',
|
|
546
556
|
sourceChannel: 'voice',
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
557
|
+
conversationExternalId: '+15556667777',
|
|
558
|
+
actorExternalId: '+15556667777',
|
|
559
|
+
actorDisplayName: 'Noah',
|
|
550
560
|
});
|
|
551
561
|
|
|
552
562
|
expect(result.notified).toBe(true);
|
|
@@ -584,9 +594,9 @@ describe('access-request-helper unit tests', () => {
|
|
|
584
594
|
const result = notifyGuardianOfAccessRequest({
|
|
585
595
|
canonicalAssistantId: 'self',
|
|
586
596
|
sourceChannel: 'telegram',
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
597
|
+
conversationExternalId: 'chat-123',
|
|
598
|
+
actorExternalId: 'unknown-user',
|
|
599
|
+
actorDisplayName: 'Alice',
|
|
590
600
|
});
|
|
591
601
|
|
|
592
602
|
expect(result.notified).toBe(true);
|