@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
|
@@ -47,6 +47,8 @@ const mockCallsConfig = {
|
|
|
47
47
|
|
|
48
48
|
mock.module('../config/loader.js', () => ({
|
|
49
49
|
getConfig: () => ({
|
|
50
|
+
ui: {},
|
|
51
|
+
|
|
50
52
|
model: 'test',
|
|
51
53
|
provider: 'test',
|
|
52
54
|
apiKeys: {},
|
|
@@ -227,8 +229,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
227
229
|
});
|
|
228
230
|
|
|
229
231
|
expect(res.status).toBe(400);
|
|
230
|
-
const body = await res.json() as { error: string };
|
|
231
|
-
expect(body.error).toContain('conversationId');
|
|
232
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
233
|
+
expect(body.error.message).toContain('conversationId');
|
|
232
234
|
|
|
233
235
|
await stopServer();
|
|
234
236
|
});
|
|
@@ -269,8 +271,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
269
271
|
});
|
|
270
272
|
|
|
271
273
|
expect(res.status).toBe(400);
|
|
272
|
-
const body = await res.json() as { error: string };
|
|
273
|
-
expect(body.error).toContain('E.164');
|
|
274
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
275
|
+
expect(body.error.message).toContain('E.164');
|
|
274
276
|
|
|
275
277
|
await stopServer();
|
|
276
278
|
});
|
|
@@ -285,8 +287,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
285
287
|
});
|
|
286
288
|
|
|
287
289
|
expect(res.status).toBe(400);
|
|
288
|
-
const body = await res.json() as { error: string };
|
|
289
|
-
expect(body.error).toContain('Invalid JSON');
|
|
290
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
291
|
+
expect(body.error.message).toContain('Invalid JSON');
|
|
290
292
|
|
|
291
293
|
await stopServer();
|
|
292
294
|
});
|
|
@@ -309,8 +311,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
309
311
|
// user_number mode requires a configured user phone number;
|
|
310
312
|
// since we haven't set one, this should return a 400 explaining why
|
|
311
313
|
expect(res.status).toBe(400);
|
|
312
|
-
const body = await res.json() as { error: string };
|
|
313
|
-
expect(body.error).toContain('user_number');
|
|
314
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
315
|
+
expect(body.error.message).toContain('user_number');
|
|
314
316
|
|
|
315
317
|
await stopServer();
|
|
316
318
|
});
|
|
@@ -364,11 +366,11 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
364
366
|
});
|
|
365
367
|
|
|
366
368
|
expect(res.status).toBe(400);
|
|
367
|
-
const body = await res.json() as { error: string };
|
|
368
|
-
expect(body.error).toContain('Invalid callerIdentityMode');
|
|
369
|
-
expect(body.error).toContain('bogus');
|
|
370
|
-
expect(body.error).toContain('assistant_number');
|
|
371
|
-
expect(body.error).toContain('user_number');
|
|
369
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
370
|
+
expect(body.error.message).toContain('Invalid callerIdentityMode');
|
|
371
|
+
expect(body.error.message).toContain('bogus');
|
|
372
|
+
expect(body.error.message).toContain('assistant_number');
|
|
373
|
+
expect(body.error.message).toContain('user_number');
|
|
372
374
|
|
|
373
375
|
await stopServer();
|
|
374
376
|
});
|
|
@@ -510,8 +512,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
510
512
|
});
|
|
511
513
|
|
|
512
514
|
expect(res.status).toBe(400);
|
|
513
|
-
const body = await res.json() as { error: string };
|
|
514
|
-
expect(body.error).toContain('Invalid JSON');
|
|
515
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
516
|
+
expect(body.error.message).toContain('Invalid JSON');
|
|
515
517
|
|
|
516
518
|
await stopServer();
|
|
517
519
|
});
|
|
@@ -533,9 +535,9 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
533
535
|
body: JSON.stringify({ answer: 'Yes, please' }),
|
|
534
536
|
});
|
|
535
537
|
|
|
536
|
-
expect(res.status).toBe(
|
|
537
|
-
const body = await res.json() as { error: string };
|
|
538
|
-
expect(body.error).toContain('
|
|
538
|
+
expect(res.status).toBe(409);
|
|
539
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
540
|
+
expect(body.error.message).toContain('No active controller');
|
|
539
541
|
|
|
540
542
|
await stopServer();
|
|
541
543
|
});
|
|
@@ -583,8 +585,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
583
585
|
});
|
|
584
586
|
|
|
585
587
|
expect(res.status).toBe(409);
|
|
586
|
-
const body = await res.json() as { error: string };
|
|
587
|
-
expect(body.error).toContain('
|
|
588
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
589
|
+
expect(body.error.message).toContain('No active controller');
|
|
588
590
|
|
|
589
591
|
await stopServer();
|
|
590
592
|
});
|
|
@@ -609,8 +611,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
609
611
|
});
|
|
610
612
|
|
|
611
613
|
expect(res.status).toBe(400);
|
|
612
|
-
const body = await res.json() as { error: string };
|
|
613
|
-
expect(body.error).toContain('Invalid JSON');
|
|
614
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
615
|
+
expect(body.error.message).toContain('Invalid JSON');
|
|
614
616
|
|
|
615
617
|
await stopServer();
|
|
616
618
|
});
|
|
@@ -633,8 +635,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
633
635
|
});
|
|
634
636
|
|
|
635
637
|
expect(res.status).toBe(400);
|
|
636
|
-
const body = await res.json() as { error: string };
|
|
637
|
-
expect(body.error).toContain('instructionText');
|
|
638
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
639
|
+
expect(body.error.message).toContain('instructionText');
|
|
638
640
|
|
|
639
641
|
await stopServer();
|
|
640
642
|
});
|
|
@@ -657,8 +659,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
657
659
|
});
|
|
658
660
|
|
|
659
661
|
expect(res.status).toBe(400);
|
|
660
|
-
const body = await res.json() as { error: string };
|
|
661
|
-
expect(body.error).toContain('instructionText');
|
|
662
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
663
|
+
expect(body.error.message).toContain('instructionText');
|
|
662
664
|
|
|
663
665
|
await stopServer();
|
|
664
666
|
});
|
|
@@ -673,8 +675,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
673
675
|
});
|
|
674
676
|
|
|
675
677
|
expect(res.status).toBe(404);
|
|
676
|
-
const body = await res.json() as { error: string };
|
|
677
|
-
expect(body.error).toContain('No call session found');
|
|
678
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
679
|
+
expect(body.error.message).toContain('No call session found');
|
|
678
680
|
|
|
679
681
|
await stopServer();
|
|
680
682
|
});
|
|
@@ -699,8 +701,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
699
701
|
});
|
|
700
702
|
|
|
701
703
|
expect(res.status).toBe(409);
|
|
702
|
-
const body = await res.json() as { error: string };
|
|
703
|
-
expect(body.error).toContain('not active');
|
|
704
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
705
|
+
expect(body.error.message).toContain('not active');
|
|
704
706
|
|
|
705
707
|
await stopServer();
|
|
706
708
|
});
|
|
@@ -723,8 +725,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
723
725
|
});
|
|
724
726
|
|
|
725
727
|
expect(res.status).toBe(409);
|
|
726
|
-
const body = await res.json() as { error: string };
|
|
727
|
-
expect(body.error).toContain('
|
|
728
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
729
|
+
expect(body.error.message).toContain('No active controller');
|
|
728
730
|
|
|
729
731
|
await stopServer();
|
|
730
732
|
});
|
|
@@ -71,7 +71,7 @@ describe('channel-invite-transport', () => {
|
|
|
71
71
|
|
|
72
72
|
describe('telegram buildShareableInvite', () => {
|
|
73
73
|
test('produces a valid Telegram deep link', () => {
|
|
74
|
-
const result = telegramInviteTransport.buildShareableInvite({
|
|
74
|
+
const result = telegramInviteTransport.buildShareableInvite!({
|
|
75
75
|
rawToken: 'abc123_test-token',
|
|
76
76
|
sourceChannel: 'telegram',
|
|
77
77
|
});
|
|
@@ -81,15 +81,15 @@ describe('channel-invite-transport', () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
test('deep link is deterministic for the same token', () => {
|
|
84
|
-
const a = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
|
|
85
|
-
const b = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
|
|
84
|
+
const a = telegramInviteTransport.buildShareableInvite!({ rawToken: 'tok1', sourceChannel: 'telegram' });
|
|
85
|
+
const b = telegramInviteTransport.buildShareableInvite!({ rawToken: 'tok1', sourceChannel: 'telegram' });
|
|
86
86
|
expect(a.url).toBe(b.url);
|
|
87
87
|
expect(a.displayText).toBe(b.displayText);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
test('uses the configured bot username', () => {
|
|
91
91
|
mockBotUsername = 'my_custom_bot';
|
|
92
|
-
const result = telegramInviteTransport.buildShareableInvite({
|
|
92
|
+
const result = telegramInviteTransport.buildShareableInvite!({
|
|
93
93
|
rawToken: 'token',
|
|
94
94
|
sourceChannel: 'telegram',
|
|
95
95
|
});
|
|
@@ -103,7 +103,7 @@ describe('channel-invite-transport', () => {
|
|
|
103
103
|
delete process.env.TELEGRAM_BOT_USERNAME;
|
|
104
104
|
try {
|
|
105
105
|
expect(() =>
|
|
106
|
-
telegramInviteTransport.buildShareableInvite({
|
|
106
|
+
telegramInviteTransport.buildShareableInvite!({
|
|
107
107
|
rawToken: 'token',
|
|
108
108
|
sourceChannel: 'telegram',
|
|
109
109
|
}),
|
|
@@ -118,7 +118,7 @@ describe('channel-invite-transport', () => {
|
|
|
118
118
|
const prev = process.env.TELEGRAM_BOT_USERNAME;
|
|
119
119
|
process.env.TELEGRAM_BOT_USERNAME = 'env_bot';
|
|
120
120
|
try {
|
|
121
|
-
const result = telegramInviteTransport.buildShareableInvite({
|
|
121
|
+
const result = telegramInviteTransport.buildShareableInvite!({
|
|
122
122
|
rawToken: 'token',
|
|
123
123
|
sourceChannel: 'telegram',
|
|
124
124
|
});
|
|
@@ -50,6 +50,25 @@ mock.module('../runtime/gateway-client.js', () => ({
|
|
|
50
50
|
}));
|
|
51
51
|
|
|
52
52
|
mock.module('../memory/conversation-store.js', () => ({
|
|
53
|
+
getConversationThreadType: () => 'default',
|
|
54
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
55
|
+
updateConversationContextWindow: () => {},
|
|
56
|
+
deleteMessageById: () => {},
|
|
57
|
+
updateConversationTitle: () => {},
|
|
58
|
+
updateConversationUsage: () => {},
|
|
59
|
+
addMessage: () => ({ id: 'mock-msg-id' }),
|
|
60
|
+
getConversation: () => ({
|
|
61
|
+
id: 'conv-1',
|
|
62
|
+
contextSummary: null,
|
|
63
|
+
contextCompactedMessageCount: 0,
|
|
64
|
+
totalInputTokens: 0,
|
|
65
|
+
totalOutputTokens: 0,
|
|
66
|
+
totalEstimatedCost: 0,
|
|
67
|
+
title: null,
|
|
68
|
+
}),
|
|
69
|
+
provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
|
|
70
|
+
getConversationOriginInterface: () => null,
|
|
71
|
+
getConversationOriginChannel: () => null,
|
|
53
72
|
getMessages: () => conversationMessages,
|
|
54
73
|
}));
|
|
55
74
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
6
|
+
import { eq } from 'drizzle-orm';
|
|
7
|
+
|
|
8
|
+
const testDir = mkdtempSync(join(tmpdir(), 'channel-retry-sweep-test-'));
|
|
9
|
+
|
|
10
|
+
mock.module('../util/platform.js', () => ({
|
|
11
|
+
getDataDir: () => testDir,
|
|
12
|
+
isMacOS: () => process.platform === 'darwin',
|
|
13
|
+
isLinux: () => process.platform === 'linux',
|
|
14
|
+
isWindows: () => process.platform === 'win32',
|
|
15
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
16
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
17
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
18
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
19
|
+
ensureDataDir: () => {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module('../util/logger.js', () => ({
|
|
23
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
24
|
+
get: () => () => {},
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
|
|
29
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
30
|
+
import { channelInboundEvents, messages } from '../memory/schema.js';
|
|
31
|
+
import { sweepFailedEvents } from '../runtime/channel-retry-sweep.js';
|
|
32
|
+
|
|
33
|
+
initializeDb();
|
|
34
|
+
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
resetDb();
|
|
37
|
+
try {
|
|
38
|
+
rmSync(testDir, { recursive: true });
|
|
39
|
+
} catch {
|
|
40
|
+
// Best effort cleanup
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function resetTables(): void {
|
|
45
|
+
const db = getDb();
|
|
46
|
+
db.run('DELETE FROM channel_inbound_events');
|
|
47
|
+
db.run('DELETE FROM conversation_keys');
|
|
48
|
+
db.run('DELETE FROM messages');
|
|
49
|
+
db.run('DELETE FROM conversations');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function seedFailedLegacyEvent(actorRole: 'guardian' | 'non-guardian' | 'unverified_channel'): string {
|
|
53
|
+
const inbound = channelDeliveryStore.recordInbound('telegram', 'chat-legacy', `msg-${actorRole}`);
|
|
54
|
+
channelDeliveryStore.storePayload(inbound.eventId, {
|
|
55
|
+
content: 'retry me',
|
|
56
|
+
sourceChannel: 'telegram',
|
|
57
|
+
interface: 'telegram',
|
|
58
|
+
guardianCtx: {
|
|
59
|
+
actorRole,
|
|
60
|
+
sourceChannel: 'telegram',
|
|
61
|
+
requesterExternalUserId: 'legacy-user',
|
|
62
|
+
requesterChatId: 'chat-legacy',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const db = getDb();
|
|
67
|
+
db.update(channelInboundEvents)
|
|
68
|
+
.set({
|
|
69
|
+
processingStatus: 'failed',
|
|
70
|
+
processingAttempts: 1,
|
|
71
|
+
retryAfter: Date.now() - 1,
|
|
72
|
+
})
|
|
73
|
+
.where(eq(channelInboundEvents.id, inbound.eventId))
|
|
74
|
+
.run();
|
|
75
|
+
|
|
76
|
+
return inbound.eventId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('channel-retry-sweep', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
resetTables();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('replays legacy guardianCtx.actorRole with preserved trust semantics', async () => {
|
|
85
|
+
const cases: Array<{
|
|
86
|
+
actorRole: 'guardian' | 'non-guardian' | 'unverified_channel';
|
|
87
|
+
expectedTrustClass: 'guardian' | 'trusted_contact' | 'unknown';
|
|
88
|
+
expectedInteractive: boolean;
|
|
89
|
+
}> = [
|
|
90
|
+
{ actorRole: 'guardian', expectedTrustClass: 'guardian', expectedInteractive: true },
|
|
91
|
+
{ actorRole: 'non-guardian', expectedTrustClass: 'trusted_contact', expectedInteractive: false },
|
|
92
|
+
{ actorRole: 'unverified_channel', expectedTrustClass: 'unknown', expectedInteractive: false },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const c of cases) {
|
|
96
|
+
const eventId = seedFailedLegacyEvent(c.actorRole);
|
|
97
|
+
let capturedOptions: {
|
|
98
|
+
guardianContext?: { trustClass?: string };
|
|
99
|
+
isInteractive?: boolean;
|
|
100
|
+
} | undefined;
|
|
101
|
+
|
|
102
|
+
await sweepFailedEvents(
|
|
103
|
+
async (conversationId, _content, _attachmentIds, options) => {
|
|
104
|
+
capturedOptions = options as {
|
|
105
|
+
guardianContext?: { trustClass?: string };
|
|
106
|
+
isInteractive?: boolean;
|
|
107
|
+
};
|
|
108
|
+
const messageId = `message-${c.actorRole}`;
|
|
109
|
+
const db = getDb();
|
|
110
|
+
db.insert(messages).values({
|
|
111
|
+
id: messageId,
|
|
112
|
+
conversationId,
|
|
113
|
+
role: 'user',
|
|
114
|
+
content: JSON.stringify([{ type: 'text', text: 'retry me' }]),
|
|
115
|
+
createdAt: Date.now(),
|
|
116
|
+
}).run();
|
|
117
|
+
return { messageId };
|
|
118
|
+
},
|
|
119
|
+
undefined,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(capturedOptions?.guardianContext?.trustClass).toBe(c.expectedTrustClass);
|
|
123
|
+
expect(capturedOptions?.isInteractive).toBe(c.expectedInteractive);
|
|
124
|
+
|
|
125
|
+
const db = getDb();
|
|
126
|
+
const row = db.select().from(channelInboundEvents).where(eq(channelInboundEvents.id, eventId)).get();
|
|
127
|
+
expect(row?.processingStatus).toBe('processed');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -3,11 +3,12 @@ import { existsSync,mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
import { afterEach,beforeEach, describe, expect, test } from 'bun:test';
|
|
6
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
_resetEnrichmentService,
|
|
10
10
|
CommitEnrichmentService,
|
|
11
|
+
getEnrichmentService,
|
|
11
12
|
} from '../workspace/commit-message-enrichment-service.js';
|
|
12
13
|
import type { CommitContext } from '../workspace/commit-message-provider.js';
|
|
13
14
|
import { _resetGitServiceRegistry,WorkspaceGitService } from '../workspace/git-service.js';
|
|
@@ -27,11 +28,18 @@ describe('CommitEnrichmentService', () => {
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
afterEach(async () => {
|
|
31
|
+
try { await getEnrichmentService().shutdown(); } catch { /* ignore */ }
|
|
32
|
+
_resetEnrichmentService();
|
|
30
33
|
if (existsSync(testDir)) {
|
|
31
34
|
rmSync(testDir, { recursive: true, force: true });
|
|
32
35
|
}
|
|
33
36
|
});
|
|
34
37
|
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
try { await getEnrichmentService().shutdown(); } catch { /* ignore */ }
|
|
40
|
+
_resetEnrichmentService();
|
|
41
|
+
});
|
|
42
|
+
|
|
35
43
|
function makeContext(overrides?: Partial<CommitContext>): CommitContext {
|
|
36
44
|
return {
|
|
37
45
|
workspaceDir: testDir,
|
|
@@ -5,6 +5,8 @@ import { describe, expect, mock,test } from 'bun:test';
|
|
|
5
5
|
// (which have no trust rules in test) don't trigger approval prompts.
|
|
6
6
|
mock.module('../config/loader.js', () => ({
|
|
7
7
|
getConfig: () => ({
|
|
8
|
+
ui: {},
|
|
9
|
+
|
|
8
10
|
provider: 'mock-provider',
|
|
9
11
|
permissions: { mode: 'legacy' },
|
|
10
12
|
apiKeys: {},
|
|
@@ -3,6 +3,8 @@ import { beforeAll, describe, expect, mock,test } from 'bun:test';
|
|
|
3
3
|
// Mock config before importing modules that depend on it.
|
|
4
4
|
mock.module('../config/loader.js', () => ({
|
|
5
5
|
getConfig: () => ({
|
|
6
|
+
ui: {},
|
|
7
|
+
|
|
6
8
|
provider: 'mock-provider',
|
|
7
9
|
permissions: { mode: 'legacy' },
|
|
8
10
|
apiKeys: {},
|
|
@@ -79,7 +79,7 @@ describe('AssistantConfigSchema', () => {
|
|
|
79
79
|
expect(result.thinking).toEqual({ enabled: false, budgetTokens: 10000, streamThinking: false });
|
|
80
80
|
expect(result.contextWindow).toEqual({
|
|
81
81
|
enabled: true,
|
|
82
|
-
maxInputTokens:
|
|
82
|
+
maxInputTokens: 200000,
|
|
83
83
|
targetInputTokens: 110000,
|
|
84
84
|
compactThreshold: 0.8,
|
|
85
85
|
preserveRecentUserTurns: 8,
|
|
@@ -1098,7 +1098,7 @@ describe('loadConfig with schema validation', () => {
|
|
|
1098
1098
|
expect(config.thinking).toEqual({ enabled: false, budgetTokens: 10000, streamThinking: false });
|
|
1099
1099
|
expect(config.contextWindow).toEqual({
|
|
1100
1100
|
enabled: true,
|
|
1101
|
-
maxInputTokens:
|
|
1101
|
+
maxInputTokens: 200000,
|
|
1102
1102
|
targetInputTokens: 110000,
|
|
1103
1103
|
compactThreshold: 0.8,
|
|
1104
1104
|
preserveRecentUserTurns: 8,
|
|
@@ -1188,7 +1188,7 @@ describe('loadConfig with schema validation', () => {
|
|
|
1188
1188
|
test('falls back for invalid contextWindow relationship', () => {
|
|
1189
1189
|
writeConfig({ contextWindow: { maxInputTokens: 1000, targetInputTokens: 1000 } });
|
|
1190
1190
|
const config = loadConfig();
|
|
1191
|
-
expect(config.contextWindow.maxInputTokens).toBe(
|
|
1191
|
+
expect(config.contextWindow.maxInputTokens).toBe(200000);
|
|
1192
1192
|
expect(config.contextWindow.targetInputTokens).toBe(110000);
|
|
1193
1193
|
});
|
|
1194
1194
|
|
|
@@ -1348,7 +1348,7 @@ describe('Call entrypoint gating', () => {
|
|
|
1348
1348
|
const response = await handleStartCall(req);
|
|
1349
1349
|
expect(response.status).toBe(403);
|
|
1350
1350
|
|
|
1351
|
-
const body = await response.json() as { error: string };
|
|
1352
|
-
expect(body.error).toContain('disabled');
|
|
1351
|
+
const body = await response.json() as { error: { message: string } };
|
|
1352
|
+
expect(body.error.message).toContain('disabled');
|
|
1353
1353
|
});
|
|
1354
1354
|
});
|
|
@@ -96,7 +96,9 @@ mock.module('node:fs', () => {
|
|
|
96
96
|
|
|
97
97
|
// Mock config/loader and other dependencies that ConfigWatcher imports
|
|
98
98
|
mock.module('../config/loader.js', () => ({
|
|
99
|
-
getConfig: () => ({
|
|
99
|
+
getConfig: () => ({
|
|
100
|
+
ui: {},
|
|
101
|
+
}),
|
|
100
102
|
invalidateConfigCache: () => {},
|
|
101
103
|
}));
|
|
102
104
|
|
|
@@ -41,21 +41,30 @@ describe('hasNoAuthOverride', () => {
|
|
|
41
41
|
expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: 'false' })).toBe(false);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test('returns true when VELLUM_DAEMON_NOAUTH is 1', () => {
|
|
45
|
-
expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: '1' })).toBe(true);
|
|
44
|
+
test('returns true when VELLUM_DAEMON_NOAUTH is 1 with safety gate', () => {
|
|
45
|
+
expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: '1', VELLUM_UNSAFE_AUTH_BYPASS: '1' })).toBe(true);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
test('returns
|
|
49
|
-
expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: '
|
|
48
|
+
test('returns false when VELLUM_DAEMON_NOAUTH is 1 without safety gate', () => {
|
|
49
|
+
expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: '1' })).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('returns true when VELLUM_DAEMON_NOAUTH is true with safety gate', () => {
|
|
53
|
+
expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: 'true', VELLUM_UNSAFE_AUTH_BYPASS: '1' })).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('returns false when VELLUM_DAEMON_NOAUTH is true without safety gate', () => {
|
|
57
|
+
expect(hasNoAuthOverride({ VELLUM_DAEMON_NOAUTH: 'true' })).toBe(false);
|
|
50
58
|
});
|
|
51
59
|
|
|
52
60
|
test('is independent of VELLUM_DAEMON_SOCKET', () => {
|
|
53
61
|
// Socket override alone does NOT enable no-auth
|
|
54
62
|
expect(hasNoAuthOverride({ VELLUM_DAEMON_SOCKET: '/tmp/custom.sock' })).toBe(false);
|
|
55
|
-
// No-auth requires its own explicit flag
|
|
63
|
+
// No-auth requires its own explicit flag plus safety gate
|
|
56
64
|
expect(hasNoAuthOverride({
|
|
57
65
|
VELLUM_DAEMON_SOCKET: '/tmp/custom.sock',
|
|
58
66
|
VELLUM_DAEMON_NOAUTH: '1',
|
|
67
|
+
VELLUM_UNSAFE_AUTH_BYPASS: '1',
|
|
59
68
|
})).toBe(true);
|
|
60
69
|
});
|
|
61
70
|
});
|
|
@@ -49,6 +49,16 @@ const getConversationMock = mock((id: string) => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
mock.module('../memory/conversation-store.js', () => ({
|
|
52
|
+
getConversationThreadType: () => 'default',
|
|
53
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
54
|
+
updateConversationContextWindow: () => {},
|
|
55
|
+
deleteMessageById: () => {},
|
|
56
|
+
updateConversationTitle: () => {},
|
|
57
|
+
updateConversationUsage: () => {},
|
|
58
|
+
getMessages: () => [],
|
|
59
|
+
provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
|
|
60
|
+
getConversationOriginInterface: () => null,
|
|
61
|
+
getConversationOriginChannel: () => null,
|
|
52
62
|
createConversation: createConversationMock,
|
|
53
63
|
addMessage: addMessageMock,
|
|
54
64
|
getConversation: getConversationMock,
|
|
@@ -49,7 +49,7 @@ describe('handleSendMessage', () => {
|
|
|
49
49
|
expect(body.messageId).toBe('msg-legacy-fallback');
|
|
50
50
|
expect(capturedSourceChannel).toBe('telegram');
|
|
51
51
|
expect(capturedOptions?.guardianContext).toEqual({
|
|
52
|
-
|
|
52
|
+
trustClass: 'guardian',
|
|
53
53
|
sourceChannel: 'telegram',
|
|
54
54
|
});
|
|
55
55
|
});
|
|
@@ -61,6 +61,7 @@ mock.module('../tools/registry.js', () => ({
|
|
|
61
61
|
// Imports under test
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
|
|
64
|
+
import { DEFAULT_CONFIG } from '../config/defaults.js';
|
|
64
65
|
import { redactSensitiveFields } from '../security/redaction.js';
|
|
65
66
|
import { setSecureKey } from '../security/secure-keys.js';
|
|
66
67
|
import { CredentialBroker } from '../tools/credentials/broker.js';
|
|
@@ -125,7 +126,18 @@ describe('Invariant 1: secrets never enter LLM context', () => {
|
|
|
125
126
|
test('user message containing secret is blocked from entering history', () => {
|
|
126
127
|
// Mock config to enable block mode
|
|
127
128
|
mock.module('../config/loader.js', () => ({
|
|
129
|
+
applyNestedDefaults: (config: unknown) => config,
|
|
128
130
|
getConfig: () => ({
|
|
131
|
+
ui: {},
|
|
132
|
+
secretDetection: {
|
|
133
|
+
enabled: true,
|
|
134
|
+
action: 'block',
|
|
135
|
+
blockIngress: true,
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
invalidateConfigCache: () => {},
|
|
139
|
+
loadConfig: () => ({
|
|
140
|
+
ui: {},
|
|
129
141
|
secretDetection: {
|
|
130
142
|
enabled: true,
|
|
131
143
|
action: 'block',
|
|
@@ -204,6 +216,10 @@ describe('Invariant 2: no generic plaintext secret read API', () => {
|
|
|
204
216
|
'messaging/providers/telegram-bot/adapter.ts', // Telegram bot token lookup for connectivity check
|
|
205
217
|
'messaging/providers/sms/adapter.ts', // Twilio credential lookup for SMS connectivity check
|
|
206
218
|
'runtime/channel-readiness-service.ts', // channel readiness probes for SMS/Telegram connectivity
|
|
219
|
+
'messaging/providers/whatsapp/adapter.ts', // WhatsApp credential lookup for connectivity check
|
|
220
|
+
'schedule/integration-status.ts', // integration status checks for scheduled reports
|
|
221
|
+
'daemon/handlers/oauth-connect.ts', // OAuth connect handler for integration setup
|
|
222
|
+
'daemon/handlers/config-slack-channel.ts', // Slack channel config credential management
|
|
207
223
|
]);
|
|
208
224
|
|
|
209
225
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -436,20 +452,14 @@ describe('One-time send override', () => {
|
|
|
436
452
|
});
|
|
437
453
|
|
|
438
454
|
test('allowOneTimeSend defaults to false in config', () => {
|
|
439
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
440
|
-
const { DEFAULT_CONFIG } = require('../config/defaults.js');
|
|
441
455
|
expect(DEFAULT_CONFIG.secretDetection.allowOneTimeSend).toBe(false);
|
|
442
456
|
});
|
|
443
457
|
|
|
444
458
|
test('default secretDetection.action is redact', () => {
|
|
445
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
446
|
-
const { DEFAULT_CONFIG } = require('../config/defaults.js');
|
|
447
459
|
expect(DEFAULT_CONFIG.secretDetection.action).toBe('redact');
|
|
448
460
|
});
|
|
449
461
|
|
|
450
462
|
test('default secretDetection.blockIngress is true', () => {
|
|
451
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
452
|
-
const { DEFAULT_CONFIG } = require('../config/defaults.js');
|
|
453
463
|
expect(DEFAULT_CONFIG.secretDetection.blockIngress).toBe(true);
|
|
454
464
|
});
|
|
455
465
|
});
|