@vellumai/assistant 0.3.28 → 0.4.0
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 +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +50 -12
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -92,7 +92,7 @@ describe('ingress member HTTP routes', () => {
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
const res = await handleUpsertMember(req);
|
|
95
|
-
const body = await res.json() as
|
|
95
|
+
const body = await res.json() as { ok: boolean; error: string };
|
|
96
96
|
|
|
97
97
|
expect(res.status).toBe(400);
|
|
98
98
|
expect(body.ok).toBe(false);
|
|
@@ -109,7 +109,7 @@ describe('ingress member HTTP routes', () => {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
const res = await handleUpsertMember(req);
|
|
112
|
-
const body = await res.json() as
|
|
112
|
+
const body = await res.json() as { ok: boolean; error: string };
|
|
113
113
|
|
|
114
114
|
expect(res.status).toBe(400);
|
|
115
115
|
expect(body.ok).toBe(false);
|
|
@@ -278,6 +278,42 @@ describe('ingress invite HTTP routes', () => {
|
|
|
278
278
|
expect((invite.token as string).length).toBeGreaterThan(0);
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
+
test('POST /v1/ingress/invites — includes canonical share URL when bot username is configured', async () => {
|
|
282
|
+
const prevBotUsername = process.env.TELEGRAM_BOT_USERNAME;
|
|
283
|
+
process.env.TELEGRAM_BOT_USERNAME = 'test_invite_bot';
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: { 'Content-Type': 'application/json' },
|
|
289
|
+
body: JSON.stringify({
|
|
290
|
+
sourceChannel: 'telegram',
|
|
291
|
+
note: 'Share link test',
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const res = await handleCreateInvite(req);
|
|
296
|
+
const body = await res.json() as Record<string, unknown>;
|
|
297
|
+
const invite = body.invite as Record<string, unknown>;
|
|
298
|
+
const token = invite.token as string;
|
|
299
|
+
const share = invite.share as Record<string, unknown>;
|
|
300
|
+
|
|
301
|
+
expect(res.status).toBe(201);
|
|
302
|
+
expect(body.ok).toBe(true);
|
|
303
|
+
expect(typeof token).toBe('string');
|
|
304
|
+
expect(token.length).toBeGreaterThan(0);
|
|
305
|
+
expect(share).toBeDefined();
|
|
306
|
+
expect(share.url).toBe(`https://t.me/test_invite_bot?start=iv_${token}`);
|
|
307
|
+
expect(typeof share.displayText).toBe('string');
|
|
308
|
+
} finally {
|
|
309
|
+
if (prevBotUsername === undefined) {
|
|
310
|
+
delete process.env.TELEGRAM_BOT_USERNAME;
|
|
311
|
+
} else {
|
|
312
|
+
process.env.TELEGRAM_BOT_USERNAME = prevBotUsername;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
281
317
|
test('POST /v1/ingress/invites — missing sourceChannel returns 400', async () => {
|
|
282
318
|
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
283
319
|
method: 'POST',
|
|
@@ -286,7 +322,7 @@ describe('ingress invite HTTP routes', () => {
|
|
|
286
322
|
});
|
|
287
323
|
|
|
288
324
|
const res = await handleCreateInvite(req);
|
|
289
|
-
const body = await res.json() as
|
|
325
|
+
const body = await res.json() as { ok: boolean; error: string };
|
|
290
326
|
|
|
291
327
|
expect(res.status).toBe(400);
|
|
292
328
|
expect(body.ok).toBe(false);
|
|
@@ -376,7 +412,7 @@ describe('ingress invite HTTP routes', () => {
|
|
|
376
412
|
});
|
|
377
413
|
|
|
378
414
|
const res = await handleRedeemInvite(req);
|
|
379
|
-
const body = await res.json() as
|
|
415
|
+
const body = await res.json() as { ok: boolean; error: string };
|
|
380
416
|
|
|
381
417
|
expect(res.status).toBe(400);
|
|
382
418
|
expect(body.ok).toBe(false);
|
|
@@ -441,3 +477,194 @@ describe('ingress service shared logic', () => {
|
|
|
441
477
|
expect(revoked.invite.id).toBe(created.invite.id);
|
|
442
478
|
});
|
|
443
479
|
});
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Voice invite routes
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
describe('voice invite HTTP routes', () => {
|
|
486
|
+
beforeEach(resetTables);
|
|
487
|
+
|
|
488
|
+
test('POST /v1/ingress/invites with sourceChannel voice — creates invite with voiceCode, stores hash only', async () => {
|
|
489
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
490
|
+
method: 'POST',
|
|
491
|
+
headers: { 'Content-Type': 'application/json' },
|
|
492
|
+
body: JSON.stringify({
|
|
493
|
+
sourceChannel: 'voice',
|
|
494
|
+
expectedExternalUserId: '+15551234567',
|
|
495
|
+
maxUses: 3,
|
|
496
|
+
}),
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const res = await handleCreateInvite(req);
|
|
500
|
+
const body = await res.json() as Record<string, unknown>;
|
|
501
|
+
|
|
502
|
+
expect(res.status).toBe(201);
|
|
503
|
+
expect(body.ok).toBe(true);
|
|
504
|
+
const invite = body.invite as Record<string, unknown>;
|
|
505
|
+
expect(invite.sourceChannel).toBe('voice');
|
|
506
|
+
// Voice code should be returned (6 digits by default)
|
|
507
|
+
expect(typeof invite.voiceCode).toBe('string');
|
|
508
|
+
expect((invite.voiceCode as string).length).toBe(6);
|
|
509
|
+
expect(/^\d{6}$/.test(invite.voiceCode as string)).toBe(true);
|
|
510
|
+
// Hash should be stored
|
|
511
|
+
expect(typeof invite.tokenHash).toBe('string');
|
|
512
|
+
expect((invite.tokenHash as string).length).toBeGreaterThan(0);
|
|
513
|
+
// voiceCodeDigits should be recorded
|
|
514
|
+
expect(invite.voiceCodeDigits).toBe(6);
|
|
515
|
+
// expectedExternalUserId should be recorded
|
|
516
|
+
expect(invite.expectedExternalUserId).toBe('+15551234567');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('voice invite creation requires expectedExternalUserId', async () => {
|
|
520
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
521
|
+
method: 'POST',
|
|
522
|
+
headers: { 'Content-Type': 'application/json' },
|
|
523
|
+
body: JSON.stringify({
|
|
524
|
+
sourceChannel: 'voice',
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const res = await handleCreateInvite(req);
|
|
529
|
+
const body = await res.json() as Record<string, unknown>;
|
|
530
|
+
|
|
531
|
+
expect(res.status).toBe(400);
|
|
532
|
+
expect(body.ok).toBe(false);
|
|
533
|
+
expect(body.error).toContain('expectedExternalUserId');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('voice invite creation validates E.164 format', async () => {
|
|
537
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: { 'Content-Type': 'application/json' },
|
|
540
|
+
body: JSON.stringify({
|
|
541
|
+
sourceChannel: 'voice',
|
|
542
|
+
expectedExternalUserId: 'not-a-phone-number',
|
|
543
|
+
}),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const res = await handleCreateInvite(req);
|
|
547
|
+
const body = await res.json() as Record<string, unknown>;
|
|
548
|
+
|
|
549
|
+
expect(res.status).toBe(400);
|
|
550
|
+
expect(body.ok).toBe(false);
|
|
551
|
+
expect(body.error).toContain('E.164');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('voiceCodeDigits is always 6 — custom values are ignored', async () => {
|
|
555
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
556
|
+
method: 'POST',
|
|
557
|
+
headers: { 'Content-Type': 'application/json' },
|
|
558
|
+
body: JSON.stringify({
|
|
559
|
+
sourceChannel: 'voice',
|
|
560
|
+
expectedExternalUserId: '+15551234567',
|
|
561
|
+
voiceCodeDigits: 8,
|
|
562
|
+
}),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const res = await handleCreateInvite(req);
|
|
566
|
+
const body = await res.json() as Record<string, unknown>;
|
|
567
|
+
|
|
568
|
+
expect(res.status).toBe(201);
|
|
569
|
+
expect(body.ok).toBe(true);
|
|
570
|
+
const invite = body.invite as Record<string, unknown>;
|
|
571
|
+
expect((invite.voiceCode as string).length).toBe(6);
|
|
572
|
+
expect(invite.voiceCodeDigits).toBe(6);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test('voice invites do NOT return token in response', async () => {
|
|
576
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
577
|
+
method: 'POST',
|
|
578
|
+
headers: { 'Content-Type': 'application/json' },
|
|
579
|
+
body: JSON.stringify({
|
|
580
|
+
sourceChannel: 'voice',
|
|
581
|
+
expectedExternalUserId: '+15551234567',
|
|
582
|
+
}),
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const res = await handleCreateInvite(req);
|
|
586
|
+
const body = await res.json() as Record<string, unknown>;
|
|
587
|
+
|
|
588
|
+
expect(res.status).toBe(201);
|
|
589
|
+
const invite = body.invite as Record<string, unknown>;
|
|
590
|
+
// Voice invites must not expose the raw token — callers redeem via
|
|
591
|
+
// the identity-bound voice code flow
|
|
592
|
+
expect(invite.token).toBeUndefined();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('POST /v1/ingress/invites/redeem — redeems a voice invite code via unified endpoint', async () => {
|
|
596
|
+
// Create a voice invite
|
|
597
|
+
const createRes = await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
|
|
598
|
+
method: 'POST',
|
|
599
|
+
headers: { 'Content-Type': 'application/json' },
|
|
600
|
+
body: JSON.stringify({
|
|
601
|
+
sourceChannel: 'voice',
|
|
602
|
+
expectedExternalUserId: '+15551234567',
|
|
603
|
+
maxUses: 1,
|
|
604
|
+
}),
|
|
605
|
+
}));
|
|
606
|
+
const created = await createRes.json() as { invite: { voiceCode: string } };
|
|
607
|
+
|
|
608
|
+
// Redeem the voice code via the unified /redeem endpoint
|
|
609
|
+
const redeemReq = new Request('http://localhost/v1/ingress/invites/redeem', {
|
|
610
|
+
method: 'POST',
|
|
611
|
+
headers: { 'Content-Type': 'application/json' },
|
|
612
|
+
body: JSON.stringify({
|
|
613
|
+
callerExternalUserId: '+15551234567',
|
|
614
|
+
code: created.invite.voiceCode,
|
|
615
|
+
}),
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const res = await handleRedeemInvite(redeemReq);
|
|
619
|
+
const body = await res.json() as Record<string, unknown>;
|
|
620
|
+
|
|
621
|
+
expect(res.status).toBe(200);
|
|
622
|
+
expect(body.ok).toBe(true);
|
|
623
|
+
expect(body.type).toBe('redeemed');
|
|
624
|
+
expect(typeof body.memberId).toBe('string');
|
|
625
|
+
expect(typeof body.inviteId).toBe('string');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test('POST /v1/ingress/invites/redeem — voice code missing fields returns 400', async () => {
|
|
629
|
+
const req = new Request('http://localhost/v1/ingress/invites/redeem', {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
headers: { 'Content-Type': 'application/json' },
|
|
632
|
+
body: JSON.stringify({ callerExternalUserId: '+15551234567' }),
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const res = await handleRedeemInvite(req);
|
|
636
|
+
const body = await res.json() as Record<string, unknown>;
|
|
637
|
+
|
|
638
|
+
// No `code` and no `token` → falls through to token-based path which requires token
|
|
639
|
+
expect(res.status).toBe(400);
|
|
640
|
+
expect(body.ok).toBe(false);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test('POST /v1/ingress/invites/redeem — wrong voice code returns 400', async () => {
|
|
644
|
+
// Create a voice invite
|
|
645
|
+
await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
|
|
646
|
+
method: 'POST',
|
|
647
|
+
headers: { 'Content-Type': 'application/json' },
|
|
648
|
+
body: JSON.stringify({
|
|
649
|
+
sourceChannel: 'voice',
|
|
650
|
+
expectedExternalUserId: '+15551234567',
|
|
651
|
+
maxUses: 1,
|
|
652
|
+
}),
|
|
653
|
+
}));
|
|
654
|
+
|
|
655
|
+
const req = new Request('http://localhost/v1/ingress/invites/redeem', {
|
|
656
|
+
method: 'POST',
|
|
657
|
+
headers: { 'Content-Type': 'application/json' },
|
|
658
|
+
body: JSON.stringify({
|
|
659
|
+
callerExternalUserId: '+15551234567',
|
|
660
|
+
code: '000000',
|
|
661
|
+
}),
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const res = await handleRedeemInvite(req);
|
|
665
|
+
const body = await res.json() as Record<string, unknown>;
|
|
666
|
+
|
|
667
|
+
expect(res.status).toBe(400);
|
|
668
|
+
expect(body.ok).toBe(false);
|
|
669
|
+
});
|
|
670
|
+
});
|
|
@@ -830,6 +830,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
830
830
|
{ filename: 'chart.png', mimeType: 'image/png', data: 'iVBORw0K' },
|
|
831
831
|
],
|
|
832
832
|
},
|
|
833
|
+
message_request_complete: {
|
|
834
|
+
type: 'message_request_complete',
|
|
835
|
+
sessionId: 'sess-001',
|
|
836
|
+
requestId: 'req-inline-001',
|
|
837
|
+
runStillActive: true,
|
|
838
|
+
},
|
|
833
839
|
session_info: {
|
|
834
840
|
type: 'session_info',
|
|
835
841
|
sessionId: 'sess-001',
|
|
@@ -2179,6 +2185,13 @@ describe('IPC message snapshots', () => {
|
|
|
2179
2185
|
const complete = serverMessages.message_complete;
|
|
2180
2186
|
expect(complete.type).toBe('message_complete');
|
|
2181
2187
|
});
|
|
2188
|
+
|
|
2189
|
+
test('message_request_complete has sessionId and requestId fields', () => {
|
|
2190
|
+
const complete = serverMessages.message_request_complete;
|
|
2191
|
+
expect(complete.type).toBe('message_request_complete');
|
|
2192
|
+
expect((complete as unknown as { sessionId: string }).sessionId).toBe('sess-001');
|
|
2193
|
+
expect((complete as unknown as { requestId: string }).requestId).toBe('req-inline-001');
|
|
2194
|
+
});
|
|
2182
2195
|
});
|
|
2183
2196
|
|
|
2184
2197
|
// Baseline: session contract includes threadType metadata
|
|
@@ -19,6 +19,8 @@ let mockGenerateError: Error | null = null;
|
|
|
19
19
|
|
|
20
20
|
mock.module('../config/loader.js', () => ({
|
|
21
21
|
getConfig: () => ({
|
|
22
|
+
ui: {},
|
|
23
|
+
|
|
22
24
|
apiKeys: { gemini: mockApiKey },
|
|
23
25
|
}),
|
|
24
26
|
}));
|
|
@@ -70,6 +72,25 @@ mock.module('drizzle-orm', () => ({
|
|
|
70
72
|
}));
|
|
71
73
|
|
|
72
74
|
mock.module('../memory/conversation-store.js', () => ({
|
|
75
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
76
|
+
updateConversationContextWindow: () => {},
|
|
77
|
+
deleteMessageById: () => {},
|
|
78
|
+
updateConversationTitle: () => {},
|
|
79
|
+
updateConversationUsage: () => {},
|
|
80
|
+
addMessage: () => ({ id: 'mock-msg-id' }),
|
|
81
|
+
getMessages: () => [],
|
|
82
|
+
getConversation: () => ({
|
|
83
|
+
id: 'conv-1',
|
|
84
|
+
contextSummary: null,
|
|
85
|
+
contextCompactedMessageCount: 0,
|
|
86
|
+
totalInputTokens: 0,
|
|
87
|
+
totalOutputTokens: 0,
|
|
88
|
+
totalEstimatedCost: 0,
|
|
89
|
+
title: null,
|
|
90
|
+
}),
|
|
91
|
+
provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
|
|
92
|
+
getConversationOriginInterface: () => null,
|
|
93
|
+
getConversationOriginChannel: () => null,
|
|
73
94
|
getConversationThreadType: () => 'standard',
|
|
74
95
|
}));
|
|
75
96
|
|
|
@@ -4448,7 +4448,7 @@ describe('Memory regressions', () => {
|
|
|
4448
4448
|
role: 'user',
|
|
4449
4449
|
content: 'Untrusted sender says preferences should not become durable profile memory.',
|
|
4450
4450
|
metadata: JSON.stringify({
|
|
4451
|
-
|
|
4451
|
+
provenanceTrustClass: 'trusted_contact',
|
|
4452
4452
|
provenanceSourceChannel: 'telegram',
|
|
4453
4453
|
}),
|
|
4454
4454
|
createdAt: conv.createdAt + 1,
|
|
@@ -4516,7 +4516,7 @@ describe('Memory regressions', () => {
|
|
|
4516
4516
|
conversationId: 'conv-relation-provenance-gate',
|
|
4517
4517
|
role: 'user',
|
|
4518
4518
|
content: JSON.stringify([{ type: 'text', text: 'Trusted guardian message for relation backfill.' }]),
|
|
4519
|
-
metadata: JSON.stringify({
|
|
4519
|
+
metadata: JSON.stringify({ provenanceTrustClass: 'guardian', provenanceSourceChannel: 'telegram' }),
|
|
4520
4520
|
createdAt: now + 1,
|
|
4521
4521
|
},
|
|
4522
4522
|
{
|
|
@@ -4524,7 +4524,7 @@ describe('Memory regressions', () => {
|
|
|
4524
4524
|
conversationId: 'conv-relation-provenance-gate',
|
|
4525
4525
|
role: 'user',
|
|
4526
4526
|
content: JSON.stringify([{ type: 'text', text: 'Untrusted message that should be excluded from relation backfill extraction.' }]),
|
|
4527
|
-
metadata: JSON.stringify({
|
|
4527
|
+
metadata: JSON.stringify({ provenanceTrustClass: 'trusted_contact', provenanceSourceChannel: 'telegram' }),
|
|
4528
4528
|
createdAt: now + 2,
|
|
4529
4529
|
},
|
|
4530
4530
|
]).run();
|
|
@@ -4566,7 +4566,7 @@ describe('Memory regressions', () => {
|
|
|
4566
4566
|
const conv = createConversation('provenance-preserve');
|
|
4567
4567
|
const metadata = {
|
|
4568
4568
|
userMessageChannel: 'telegram' as const,
|
|
4569
|
-
|
|
4569
|
+
provenanceTrustClass: 'trusted_contact' as const,
|
|
4570
4570
|
provenanceSourceChannel: 'telegram' as const,
|
|
4571
4571
|
provenanceGuardianExternalUserId: 'guardian-123',
|
|
4572
4572
|
provenanceRequesterIdentifier: 'Alice',
|
|
@@ -4582,7 +4582,7 @@ describe('Memory regressions', () => {
|
|
|
4582
4582
|
|
|
4583
4583
|
expect(stored).toBeTruthy();
|
|
4584
4584
|
const parsed = JSON.parse(stored!.metadata!);
|
|
4585
|
-
expect(parsed.
|
|
4585
|
+
expect(parsed.provenanceTrustClass).toBe('trusted_contact');
|
|
4586
4586
|
expect(parsed.provenanceSourceChannel).toBe('telegram');
|
|
4587
4587
|
expect(parsed.provenanceGuardianExternalUserId).toBe('guardian-123');
|
|
4588
4588
|
expect(parsed.provenanceRequesterIdentifier).toBe('Alice');
|
|
@@ -4590,13 +4590,13 @@ describe('Memory regressions', () => {
|
|
|
4590
4590
|
|
|
4591
4591
|
test('messageMetadataSchema validates provenance fields', () => {
|
|
4592
4592
|
const valid = messageMetadataSchema.safeParse({
|
|
4593
|
-
|
|
4594
|
-
provenanceSourceChannel: '
|
|
4593
|
+
provenanceTrustClass: 'guardian',
|
|
4594
|
+
provenanceSourceChannel: 'vellum',
|
|
4595
4595
|
});
|
|
4596
4596
|
expect(valid.success).toBe(true);
|
|
4597
4597
|
|
|
4598
4598
|
const validNonGuardian = messageMetadataSchema.safeParse({
|
|
4599
|
-
|
|
4599
|
+
provenanceTrustClass: 'trusted_contact',
|
|
4600
4600
|
provenanceSourceChannel: 'telegram',
|
|
4601
4601
|
provenanceGuardianExternalUserId: 'g-123',
|
|
4602
4602
|
provenanceRequesterIdentifier: 'Bob',
|
|
@@ -4604,41 +4604,41 @@ describe('Memory regressions', () => {
|
|
|
4604
4604
|
expect(validNonGuardian.success).toBe(true);
|
|
4605
4605
|
|
|
4606
4606
|
const validUnverified = messageMetadataSchema.safeParse({
|
|
4607
|
-
|
|
4607
|
+
provenanceTrustClass: 'unknown',
|
|
4608
4608
|
});
|
|
4609
4609
|
expect(validUnverified.success).toBe(true);
|
|
4610
4610
|
});
|
|
4611
4611
|
|
|
4612
4612
|
test('provenanceFromGuardianContext returns unverified_channel default when no context', () => {
|
|
4613
4613
|
const result = provenanceFromGuardianContext(null);
|
|
4614
|
-
expect(result.
|
|
4614
|
+
expect(result.provenanceTrustClass).toBe('unknown');
|
|
4615
4615
|
expect(result.provenanceSourceChannel).toBeUndefined();
|
|
4616
4616
|
|
|
4617
4617
|
const result2 = provenanceFromGuardianContext(undefined);
|
|
4618
|
-
expect(result2.
|
|
4618
|
+
expect(result2.provenanceTrustClass).toBe('unknown');
|
|
4619
4619
|
});
|
|
4620
4620
|
|
|
4621
4621
|
test('provenanceFromGuardianContext extracts fields from guardian context', () => {
|
|
4622
4622
|
const ctx = {
|
|
4623
4623
|
sourceChannel: 'telegram' as const,
|
|
4624
|
-
|
|
4624
|
+
trustClass: 'trusted_contact' as const,
|
|
4625
4625
|
guardianExternalUserId: 'g-456',
|
|
4626
4626
|
requesterIdentifier: 'Charlie',
|
|
4627
4627
|
};
|
|
4628
4628
|
const result = provenanceFromGuardianContext(ctx);
|
|
4629
|
-
expect(result.
|
|
4629
|
+
expect(result.provenanceTrustClass).toBe('trusted_contact');
|
|
4630
4630
|
expect(result.provenanceSourceChannel).toBe('telegram');
|
|
4631
4631
|
expect(result.provenanceGuardianExternalUserId).toBe('g-456');
|
|
4632
4632
|
expect(result.provenanceRequesterIdentifier).toBe('Charlie');
|
|
4633
4633
|
});
|
|
4634
4634
|
|
|
4635
|
-
test('indexMessageNow receives
|
|
4635
|
+
test('indexMessageNow receives provenanceTrustClass when metadata includes it', async () => {
|
|
4636
4636
|
const conv = createConversation('provenance-indexer');
|
|
4637
4637
|
const metadata = {
|
|
4638
|
-
|
|
4638
|
+
provenanceTrustClass: 'trusted_contact' as const,
|
|
4639
4639
|
provenanceSourceChannel: 'telegram' as const,
|
|
4640
4640
|
};
|
|
4641
|
-
// addMessage parses metadata and passes
|
|
4641
|
+
// addMessage parses metadata and passes provenanceTrustClass to indexMessageNow.
|
|
4642
4642
|
// We verify indirectly: the message is persisted with metadata and segments are indexed.
|
|
4643
4643
|
const msg = await addMessage(conv.id, 'user', 'Test provenance indexing message with enough content to segment', metadata);
|
|
4644
4644
|
expect(msg.id).toBeTruthy();
|
|
@@ -4683,7 +4683,7 @@ describe('Memory regressions', () => {
|
|
|
4683
4683
|
role: 'user',
|
|
4684
4684
|
content: JSON.stringify([{ type: 'text', text: 'Untrusted user preference for dark mode.' }]),
|
|
4685
4685
|
createdAt: now,
|
|
4686
|
-
|
|
4686
|
+
provenanceTrustClass: 'trusted_contact',
|
|
4687
4687
|
}, DEFAULT_CONFIG.memory);
|
|
4688
4688
|
|
|
4689
4689
|
expect(result.indexedSegments).toBeGreaterThan(0);
|
|
@@ -4733,7 +4733,7 @@ describe('Memory regressions', () => {
|
|
|
4733
4733
|
role: 'user',
|
|
4734
4734
|
content: JSON.stringify([{ type: 'text', text: 'Trusted guardian preference for light mode.' }]),
|
|
4735
4735
|
createdAt: now,
|
|
4736
|
-
|
|
4736
|
+
provenanceTrustClass: 'guardian',
|
|
4737
4737
|
}, DEFAULT_CONFIG.memory);
|
|
4738
4738
|
|
|
4739
4739
|
expect(result.indexedSegments).toBeGreaterThan(0);
|
|
@@ -4779,7 +4779,7 @@ describe('Memory regressions', () => {
|
|
|
4779
4779
|
role: 'user',
|
|
4780
4780
|
content: JSON.stringify([{ type: 'text', text: 'Legacy message with no provenance info.' }]),
|
|
4781
4781
|
createdAt: now,
|
|
4782
|
-
//
|
|
4782
|
+
// provenanceTrustClass is intentionally omitted (undefined) for backwards compat
|
|
4783
4783
|
}, DEFAULT_CONFIG.memory);
|
|
4784
4784
|
|
|
4785
4785
|
expect(result.indexedSegments).toBeGreaterThan(0);
|
|
@@ -4824,7 +4824,7 @@ describe('Memory regressions', () => {
|
|
|
4824
4824
|
role: 'user',
|
|
4825
4825
|
content: JSON.stringify([{ type: 'text', text: 'Unverified channel preference for compact layout.' }]),
|
|
4826
4826
|
createdAt: now,
|
|
4827
|
-
|
|
4827
|
+
provenanceTrustClass: 'unknown',
|
|
4828
4828
|
}, DEFAULT_CONFIG.memory);
|
|
4829
4829
|
|
|
4830
4830
|
expect(result.indexedSegments).toBeGreaterThan(0);
|