@vellumai/assistant 0.3.14 → 0.3.16
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 +142 -0
- package/Dockerfile +2 -2
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +8 -8
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-executor.ts +1 -1
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +6 -7
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +4 -7
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +5 -4
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +82 -20
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +2 -2
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -334,14 +334,18 @@ describe('dynamic skill tool registry', () => {
|
|
|
334
334
|
expect(getTool('sk_tool_b')?.origin).toBe('skill');
|
|
335
335
|
});
|
|
336
336
|
|
|
337
|
-
test('
|
|
337
|
+
test('skips skill tool that collides with a core tool without throwing', async () => {
|
|
338
338
|
await initializeTools();
|
|
339
339
|
|
|
340
340
|
// host_file_read is a core tool registered during init
|
|
341
341
|
const colliding = makeSkillTool('host_file_read', 'rogue-skill');
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
const accepted = registerSkillTools([colliding]);
|
|
343
|
+
|
|
344
|
+
// The colliding tool should be silently skipped
|
|
345
|
+
expect(accepted).toHaveLength(0);
|
|
346
|
+
// The core tool should still be in place (not overwritten)
|
|
347
|
+
const retrieved = getTool('host_file_read');
|
|
348
|
+
expect(retrieved?.origin).toBeUndefined(); // core tools have no origin
|
|
345
349
|
});
|
|
346
350
|
|
|
347
351
|
test('allows replacement within the same owning skill', () => {
|
|
@@ -410,7 +414,7 @@ describe('dynamic skill tool registry', () => {
|
|
|
410
414
|
expect(skillNames).not.toContain('bash');
|
|
411
415
|
});
|
|
412
416
|
|
|
413
|
-
test('registerSkillTools
|
|
417
|
+
test('registerSkillTools skips core-colliding tools but registers the rest', async () => {
|
|
414
418
|
await initializeTools();
|
|
415
419
|
|
|
416
420
|
const tools = [
|
|
@@ -418,9 +422,14 @@ describe('dynamic skill tool registry', () => {
|
|
|
418
422
|
makeSkillTool('host_file_read', 'atomic-skill'), // collides with core
|
|
419
423
|
];
|
|
420
424
|
|
|
421
|
-
|
|
422
|
-
//
|
|
423
|
-
expect(
|
|
425
|
+
const accepted = registerSkillTools(tools);
|
|
426
|
+
// Only the non-colliding tool should be accepted
|
|
427
|
+
expect(accepted).toHaveLength(1);
|
|
428
|
+
expect(accepted[0].name).toBe('sk_atomic_ok');
|
|
429
|
+
// The non-colliding tool should be registered
|
|
430
|
+
expect(getTool('sk_atomic_ok')).toBeDefined();
|
|
431
|
+
// The core tool should be untouched
|
|
432
|
+
expect(getTool('host_file_read')?.origin).toBeUndefined();
|
|
424
433
|
});
|
|
425
434
|
});
|
|
426
435
|
|
|
@@ -1519,4 +1519,109 @@ describe('relay-server', () => {
|
|
|
1519
1519
|
|
|
1520
1520
|
relay.destroy();
|
|
1521
1521
|
});
|
|
1522
|
+
|
|
1523
|
+
// ── Outbound guardian verification pointer messages ─────────────────
|
|
1524
|
+
|
|
1525
|
+
test('outbound guardian verification success emits pointer to origin conversation', async () => {
|
|
1526
|
+
ensureConversation('conv-gv-pointer-success');
|
|
1527
|
+
ensureConversation('conv-gv-pointer-success-origin');
|
|
1528
|
+
const session = createCallSession({
|
|
1529
|
+
conversationId: 'conv-gv-pointer-success',
|
|
1530
|
+
provider: 'twilio',
|
|
1531
|
+
fromNumber: '+15551111111',
|
|
1532
|
+
toNumber: '+15559999999',
|
|
1533
|
+
assistantId: 'test-assistant',
|
|
1534
|
+
callMode: 'guardian_verification',
|
|
1535
|
+
guardianVerificationSessionId: 'gv-session-ptr-success',
|
|
1536
|
+
initiatedFromConversationId: 'conv-gv-pointer-success-origin',
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
const challenge = createVerificationChallenge('test-assistant', 'voice');
|
|
1540
|
+
const secret = challenge.secret;
|
|
1541
|
+
|
|
1542
|
+
const { relay } = createMockWs(session.id);
|
|
1543
|
+
|
|
1544
|
+
await relay.handleMessage(JSON.stringify({
|
|
1545
|
+
type: 'setup',
|
|
1546
|
+
callSid: 'CA_gv_pointer_success',
|
|
1547
|
+
from: '+15551111111',
|
|
1548
|
+
to: '+15559999999',
|
|
1549
|
+
customParameters: { guardianVerificationSessionId: 'gv-session-ptr-success' },
|
|
1550
|
+
}));
|
|
1551
|
+
|
|
1552
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1553
|
+
|
|
1554
|
+
// Enter the correct code via DTMF
|
|
1555
|
+
for (const digit of secret) {
|
|
1556
|
+
await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Verification should have succeeded
|
|
1560
|
+
expect(relay.isGuardianVerificationActive()).toBe(false);
|
|
1561
|
+
|
|
1562
|
+
// Origin conversation should have a pointer message
|
|
1563
|
+
const originText = getLatestAssistantText('conv-gv-pointer-success-origin');
|
|
1564
|
+
expect(originText).not.toBeNull();
|
|
1565
|
+
expect(originText).toContain('Guardian verification');
|
|
1566
|
+
expect(originText).toContain('+15559999999');
|
|
1567
|
+
expect(originText).toContain('succeeded');
|
|
1568
|
+
|
|
1569
|
+
// Let the delayed endSession callback flush
|
|
1570
|
+
await new Promise((resolve) => setTimeout(resolve, 3100));
|
|
1571
|
+
|
|
1572
|
+
relay.destroy();
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
test('outbound guardian verification failure emits pointer to origin conversation', async () => {
|
|
1576
|
+
ensureConversation('conv-gv-pointer-fail');
|
|
1577
|
+
ensureConversation('conv-gv-pointer-fail-origin');
|
|
1578
|
+
const session = createCallSession({
|
|
1579
|
+
conversationId: 'conv-gv-pointer-fail',
|
|
1580
|
+
provider: 'twilio',
|
|
1581
|
+
fromNumber: '+15551111111',
|
|
1582
|
+
toNumber: '+15559999999',
|
|
1583
|
+
assistantId: 'test-assistant',
|
|
1584
|
+
callMode: 'guardian_verification',
|
|
1585
|
+
guardianVerificationSessionId: 'gv-session-ptr-fail',
|
|
1586
|
+
initiatedFromConversationId: 'conv-gv-pointer-fail-origin',
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
createVerificationChallenge('test-assistant', 'voice');
|
|
1590
|
+
|
|
1591
|
+
const { relay } = createMockWs(session.id);
|
|
1592
|
+
|
|
1593
|
+
await relay.handleMessage(JSON.stringify({
|
|
1594
|
+
type: 'setup',
|
|
1595
|
+
callSid: 'CA_gv_pointer_fail',
|
|
1596
|
+
from: '+15551111111',
|
|
1597
|
+
to: '+15559999999',
|
|
1598
|
+
customParameters: { guardianVerificationSessionId: 'gv-session-ptr-fail' },
|
|
1599
|
+
}));
|
|
1600
|
+
|
|
1601
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1602
|
+
|
|
1603
|
+
// Enter wrong codes 3 times (max attempts = 3)
|
|
1604
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1605
|
+
for (const digit of '000000') {
|
|
1606
|
+
await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Call should be marked as failed
|
|
1611
|
+
const updated = getCallSession(session.id);
|
|
1612
|
+
expect(updated).not.toBeNull();
|
|
1613
|
+
expect(updated!.status).toBe('failed');
|
|
1614
|
+
|
|
1615
|
+
// Origin conversation should have a failure pointer message
|
|
1616
|
+
const originText = getLatestAssistantText('conv-gv-pointer-fail-origin');
|
|
1617
|
+
expect(originText).not.toBeNull();
|
|
1618
|
+
expect(originText).toContain('Guardian verification');
|
|
1619
|
+
expect(originText).toContain('+15559999999');
|
|
1620
|
+
expect(originText).toContain('failed');
|
|
1621
|
+
|
|
1622
|
+
// Let the delayed endSession callback flush
|
|
1623
|
+
await new Promise((resolve) => setTimeout(resolve, 2100));
|
|
1624
|
+
|
|
1625
|
+
relay.destroy();
|
|
1626
|
+
});
|
|
1522
1627
|
});
|
|
@@ -219,6 +219,19 @@ describe('reminder tool', () => {
|
|
|
219
219
|
expect(result.content).toContain('Routing: multi_channel');
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
test('create with routing_hints null returns error', async () => {
|
|
223
|
+
const future = new Date(Date.now() + 60_000).toISOString();
|
|
224
|
+
const result = executeReminderCreate({
|
|
225
|
+
fire_at: future,
|
|
226
|
+
label: 'Bad hints',
|
|
227
|
+
message: 'Null hints should fail',
|
|
228
|
+
routing_hints: null,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result.isError).toBe(true);
|
|
232
|
+
expect(result.content).toContain('routing_hints must be a JSON object');
|
|
233
|
+
});
|
|
234
|
+
|
|
222
235
|
test('create without routing fields still works (backward compat)', async () => {
|
|
223
236
|
const future = new Date(Date.now() + 60_000).toISOString();
|
|
224
237
|
const result = executeReminderCreate({
|
|
@@ -85,8 +85,8 @@ describe('Runtime attachment metadata', () => {
|
|
|
85
85
|
|
|
86
86
|
// Set up conversation and messages using "self" as the assistantId
|
|
87
87
|
const mapping = getOrCreateConversation(conversationKey);
|
|
88
|
-
conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
|
|
89
|
-
const assistantMsg = conversationStore.addMessage(
|
|
88
|
+
await conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
|
|
89
|
+
const assistantMsg = await conversationStore.addMessage(
|
|
90
90
|
mapping.conversationId,
|
|
91
91
|
'assistant',
|
|
92
92
|
JSON.stringify([{ type: 'text', text: 'Here is a chart' }]),
|
|
@@ -124,8 +124,8 @@ describe('Runtime attachment metadata', () => {
|
|
|
124
124
|
const conversationKey = 'test-conv-2';
|
|
125
125
|
|
|
126
126
|
const mapping = getOrCreateConversation(conversationKey);
|
|
127
|
-
conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
|
|
128
|
-
conversationStore.addMessage(
|
|
127
|
+
await conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
|
|
128
|
+
await conversationStore.addMessage(
|
|
129
129
|
mapping.conversationId,
|
|
130
130
|
'assistant',
|
|
131
131
|
JSON.stringify([{ type: 'text', text: 'No attachments here' }]),
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from '../schedule/schedule-store.js';
|
|
37
37
|
import { startScheduler } from '../schedule/scheduler.js';
|
|
38
38
|
import { createTask } from '../tasks/task-store.js';
|
|
39
|
+
import { getReminder, insertReminder } from '../tools/reminder/reminder-store.js';
|
|
39
40
|
|
|
40
41
|
initializeDb();
|
|
41
42
|
|
|
@@ -79,6 +80,7 @@ describe('scheduler RRULE execution', () => {
|
|
|
79
80
|
const db = getDb();
|
|
80
81
|
db.run('DELETE FROM cron_runs');
|
|
81
82
|
db.run('DELETE FROM cron_jobs');
|
|
83
|
+
db.run('DELETE FROM reminders');
|
|
82
84
|
db.run('DELETE FROM task_runs');
|
|
83
85
|
db.run('DELETE FROM tasks');
|
|
84
86
|
db.run('DELETE FROM messages');
|
|
@@ -437,4 +439,52 @@ describe('scheduler RRULE execution', () => {
|
|
|
437
439
|
expect(Math.abs(after!.nextRunAt - originalNextRunAt)).toBeLessThan(65000);
|
|
438
440
|
expect(after!.lastRunAt).not.toBeNull();
|
|
439
441
|
});
|
|
442
|
+
|
|
443
|
+
test('notify reminder passes routing metadata to notifyReminder callback', async () => {
|
|
444
|
+
const reminder = insertReminder({
|
|
445
|
+
label: 'Route this reminder',
|
|
446
|
+
message: 'Reminder body',
|
|
447
|
+
fireAt: Date.now() - 1000,
|
|
448
|
+
mode: 'notify',
|
|
449
|
+
routingIntent: 'multi_channel',
|
|
450
|
+
routingHints: {
|
|
451
|
+
requestedByUser: true,
|
|
452
|
+
channelMentions: ['telegram', 'sms'],
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const notifyCalls: Array<{
|
|
457
|
+
id: string;
|
|
458
|
+
label: string;
|
|
459
|
+
message: string;
|
|
460
|
+
routingIntent: 'single_channel' | 'multi_channel' | 'all_channels';
|
|
461
|
+
routingHints: Record<string, unknown>;
|
|
462
|
+
}> = [];
|
|
463
|
+
const notifyReminder = (payload: {
|
|
464
|
+
id: string;
|
|
465
|
+
label: string;
|
|
466
|
+
message: string;
|
|
467
|
+
routingIntent: 'single_channel' | 'multi_channel' | 'all_channels';
|
|
468
|
+
routingHints: Record<string, unknown>;
|
|
469
|
+
}) => {
|
|
470
|
+
notifyCalls.push(payload);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const scheduler = startScheduler(async () => {}, notifyReminder, () => {});
|
|
474
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
475
|
+
scheduler.stop();
|
|
476
|
+
|
|
477
|
+
expect(notifyCalls).toHaveLength(1);
|
|
478
|
+
expect(notifyCalls[0]).toEqual({
|
|
479
|
+
id: reminder.id,
|
|
480
|
+
label: reminder.label,
|
|
481
|
+
message: reminder.message,
|
|
482
|
+
routingIntent: 'multi_channel',
|
|
483
|
+
routingHints: {
|
|
484
|
+
requestedByUser: true,
|
|
485
|
+
channelMentions: ['telegram', 'sms'],
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
expect(getReminder(reminder.id)?.status).toBe('fired');
|
|
489
|
+
});
|
|
440
490
|
});
|
|
@@ -381,14 +381,14 @@ describe('getAttachmentsForMessage', () => {
|
|
|
381
381
|
db.run('DELETE FROM conversations');
|
|
382
382
|
});
|
|
383
383
|
|
|
384
|
-
function createMessage(role: string, content: string): string {
|
|
384
|
+
async function createMessage(role: string, content: string): Promise<string> {
|
|
385
385
|
const conv = createConversation('test');
|
|
386
|
-
const msg = addMessage(conv.id, role, content);
|
|
386
|
+
const msg = await addMessage(conv.id, role, content);
|
|
387
387
|
return msg.id;
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
test('returns attachments linked to a message', () => {
|
|
391
|
-
const msgId = createMessage('assistant', 'Here is a chart');
|
|
390
|
+
test('returns attachments linked to a message', async () => {
|
|
391
|
+
const msgId = await createMessage('assistant', 'Here is a chart');
|
|
392
392
|
const stored = uploadAttachment('chart.png', 'image/png', 'iVBOR');
|
|
393
393
|
linkAttachmentToMessage(msgId, stored.id, 0);
|
|
394
394
|
|
|
@@ -404,8 +404,8 @@ describe('getAttachmentsForMessage', () => {
|
|
|
404
404
|
expect(getAttachmentsForMessage('msg-nonexistent')).toEqual([]);
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
-
test('returns multiple attachments in position order', () => {
|
|
408
|
-
const msgId = createMessage('assistant', 'Two files');
|
|
407
|
+
test('returns multiple attachments in position order', async () => {
|
|
408
|
+
const msgId = await createMessage('assistant', 'Two files');
|
|
409
409
|
const a1 = uploadAttachment('first.txt', 'text/plain', 'AAAA');
|
|
410
410
|
const a2 = uploadAttachment('second.txt', 'text/plain', 'BBBB');
|
|
411
411
|
|
|
@@ -418,8 +418,8 @@ describe('getAttachmentsForMessage', () => {
|
|
|
418
418
|
expect(result[1].originalFilename).toBe('second.txt');
|
|
419
419
|
});
|
|
420
420
|
|
|
421
|
-
test('returns all attachments linked to a message', () => {
|
|
422
|
-
const msgId = createMessage('assistant', 'Mixed');
|
|
421
|
+
test('returns all attachments linked to a message', async () => {
|
|
422
|
+
const msgId = await createMessage('assistant', 'Mixed');
|
|
423
423
|
const a1 = uploadAttachment('a.png', 'image/png', 'AAAA');
|
|
424
424
|
const a2 = uploadAttachment('b.png', 'image/png', 'BBBB');
|
|
425
425
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
injectGuardianContext,
|
|
11
11
|
injectTemporalContext,
|
|
12
12
|
resolveChannelCapabilities,
|
|
13
|
+
sanitizePttActivationKey,
|
|
13
14
|
stripChannelCapabilityContext,
|
|
14
15
|
stripChannelTurnContext,
|
|
15
16
|
stripGuardianContext,
|
|
@@ -787,3 +788,51 @@ describe('applyRuntimeInjections with channelTurnContext', () => {
|
|
|
787
788
|
expect(result[0].content.length).toBe(1);
|
|
788
789
|
});
|
|
789
790
|
});
|
|
791
|
+
|
|
792
|
+
// ---------------------------------------------------------------------------
|
|
793
|
+
// sanitizePttActivationKey
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
795
|
+
|
|
796
|
+
describe('sanitizePttActivationKey', () => {
|
|
797
|
+
test('returns undefined for null/undefined input', () => {
|
|
798
|
+
expect(sanitizePttActivationKey(null)).toBeUndefined();
|
|
799
|
+
expect(sanitizePttActivationKey(undefined)).toBeUndefined();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test('passes through valid keys', () => {
|
|
803
|
+
expect(sanitizePttActivationKey('fn')).toBe('fn');
|
|
804
|
+
expect(sanitizePttActivationKey('ctrl')).toBe('ctrl');
|
|
805
|
+
expect(sanitizePttActivationKey('fn_shift')).toBe('fn_shift');
|
|
806
|
+
expect(sanitizePttActivationKey('none')).toBe('none');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test('returns "unknown" for invalid keys', () => {
|
|
810
|
+
expect(sanitizePttActivationKey('malicious\nprompt injection')).toBe('unknown');
|
|
811
|
+
expect(sanitizePttActivationKey('arbitrary_value')).toBe('unknown');
|
|
812
|
+
expect(sanitizePttActivationKey('')).toBe('unknown');
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
// resolveChannelCapabilities sanitizes pttActivationKey
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
describe('resolveChannelCapabilities with PTT metadata', () => {
|
|
821
|
+
test('sanitizes valid pttActivationKey', () => {
|
|
822
|
+
const caps = resolveChannelCapabilities('macos', 'macos', { pttActivationKey: 'fn' });
|
|
823
|
+
expect(caps.pttActivationKey).toBe('fn');
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test('sanitizes invalid pttActivationKey to unknown', () => {
|
|
827
|
+
const caps = resolveChannelCapabilities('macos', 'macos', { pttActivationKey: 'evil\nprompt' });
|
|
828
|
+
expect(caps.pttActivationKey).toBe('unknown');
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test('passes through microphonePermissionGranted', () => {
|
|
832
|
+
const caps = resolveChannelCapabilities('macos', 'macos', {
|
|
833
|
+
pttActivationKey: 'fn',
|
|
834
|
+
microphonePermissionGranted: true,
|
|
835
|
+
});
|
|
836
|
+
expect(caps.microphonePermissionGranted).toBe(true);
|
|
837
|
+
});
|
|
838
|
+
});
|
|
@@ -134,6 +134,7 @@ mock.module('../tools/registry.js', () => ({
|
|
|
134
134
|
for (const id of skillIds) {
|
|
135
135
|
mockSkillRefCount.set(id, (mockSkillRefCount.get(id) ?? 0) + 1);
|
|
136
136
|
}
|
|
137
|
+
return tools;
|
|
137
138
|
},
|
|
138
139
|
unregisterSkillTools: (skillId: string) => {
|
|
139
140
|
mockUnregisteredSkillIds.push(skillId);
|
|
@@ -96,10 +96,18 @@ mock.module('node:fs', () => ({
|
|
|
96
96
|
existsSync: () => true,
|
|
97
97
|
}));
|
|
98
98
|
|
|
99
|
+
const benchmarkRegistry = new Map<string, unknown>();
|
|
99
100
|
mock.module('../tools/registry.js', () => ({
|
|
100
|
-
registerSkillTools: () => {
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
registerSkillTools: (tools: Array<{ name: string; [k: string]: unknown }>) => {
|
|
102
|
+
for (const t of tools) benchmarkRegistry.set(t.name, t);
|
|
103
|
+
return tools;
|
|
104
|
+
},
|
|
105
|
+
unregisterSkillTools: (skillId: string) => {
|
|
106
|
+
for (const [name, t] of benchmarkRegistry) {
|
|
107
|
+
if ((t as { ownerSkillId?: string }).ownerSkillId === skillId) benchmarkRegistry.delete(name);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
getTool: (name: string) => benchmarkRegistry.get(name),
|
|
103
111
|
}));
|
|
104
112
|
|
|
105
113
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
|
|
7
|
+
const testDir = mkdtempSync(join(tmpdir(), 'slack-channel-cfg-test-'));
|
|
8
|
+
|
|
9
|
+
mock.module('../config/loader.js', () => ({
|
|
10
|
+
getConfig: () => ({}),
|
|
11
|
+
loadConfig: () => ({}),
|
|
12
|
+
loadRawConfig: () => ({}),
|
|
13
|
+
saveRawConfig: () => {},
|
|
14
|
+
saveConfig: () => {},
|
|
15
|
+
invalidateConfigCache: () => {},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module('../util/platform.js', () => ({
|
|
19
|
+
getRootDir: () => testDir,
|
|
20
|
+
getDataDir: () => testDir,
|
|
21
|
+
getIpcBlobDir: () => join(testDir, 'ipc-blobs'),
|
|
22
|
+
isMacOS: () => process.platform === 'darwin',
|
|
23
|
+
isLinux: () => process.platform === 'linux',
|
|
24
|
+
isWindows: () => process.platform === 'win32',
|
|
25
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
26
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
27
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
28
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
29
|
+
ensureDataDir: () => {},
|
|
30
|
+
readHttpToken: () => undefined,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module('../util/logger.js', () => ({
|
|
34
|
+
getLogger: () => ({
|
|
35
|
+
info: () => {},
|
|
36
|
+
warn: () => {},
|
|
37
|
+
error: () => {},
|
|
38
|
+
debug: () => {},
|
|
39
|
+
trace: () => {},
|
|
40
|
+
fatal: () => {},
|
|
41
|
+
isDebug: () => false,
|
|
42
|
+
child: () => ({
|
|
43
|
+
info: () => {},
|
|
44
|
+
warn: () => {},
|
|
45
|
+
error: () => {},
|
|
46
|
+
debug: () => {},
|
|
47
|
+
isDebug: () => false,
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Mock secure key storage
|
|
53
|
+
let secureKeyStore: Record<string, string> = {};
|
|
54
|
+
|
|
55
|
+
mock.module('../security/secure-keys.js', () => ({
|
|
56
|
+
getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
|
|
57
|
+
setSecureKey: (account: string, value: string) => {
|
|
58
|
+
secureKeyStore[account] = value;
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
deleteSecureKey: (account: string) => {
|
|
62
|
+
if (account in secureKeyStore) {
|
|
63
|
+
delete secureKeyStore[account];
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
},
|
|
68
|
+
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
69
|
+
getBackendType: () => 'encrypted',
|
|
70
|
+
isDowngradedFromKeychain: () => false,
|
|
71
|
+
_resetBackend: () => {},
|
|
72
|
+
_setBackend: () => {},
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
// Mock credential metadata store
|
|
76
|
+
let credentialMetadataStore: Array<{ service: string; field: string; accountInfo?: string }> = [];
|
|
77
|
+
|
|
78
|
+
mock.module('../tools/credentials/metadata-store.js', () => ({
|
|
79
|
+
getCredentialMetadata: (service: string, field: string) =>
|
|
80
|
+
credentialMetadataStore.find((m) => m.service === service && m.field === field) ?? undefined,
|
|
81
|
+
upsertCredentialMetadata: (service: string, field: string, policy?: Record<string, unknown>) => {
|
|
82
|
+
const existing = credentialMetadataStore.find((m) => m.service === service && m.field === field);
|
|
83
|
+
if (existing) {
|
|
84
|
+
if (policy?.accountInfo !== undefined) existing.accountInfo = policy.accountInfo as string;
|
|
85
|
+
return existing;
|
|
86
|
+
}
|
|
87
|
+
const record = { service, field, accountInfo: policy?.accountInfo as string | undefined };
|
|
88
|
+
credentialMetadataStore.push(record);
|
|
89
|
+
return record;
|
|
90
|
+
},
|
|
91
|
+
deleteCredentialMetadata: (service: string, field: string) => {
|
|
92
|
+
const idx = credentialMetadataStore.findIndex((m) => m.service === service && m.field === field);
|
|
93
|
+
if (idx !== -1) {
|
|
94
|
+
credentialMetadataStore.splice(idx, 1);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
},
|
|
99
|
+
listCredentialMetadata: () => credentialMetadataStore,
|
|
100
|
+
assertMetadataWritable: () => {},
|
|
101
|
+
_setMetadataPath: () => {},
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// Mock fetch for Slack API validation
|
|
105
|
+
const originalFetch = globalThis.fetch;
|
|
106
|
+
|
|
107
|
+
import {
|
|
108
|
+
getSlackChannelConfig,
|
|
109
|
+
setSlackChannelConfig,
|
|
110
|
+
clearSlackChannelConfig,
|
|
111
|
+
} from '../daemon/handlers/config-slack-channel.js';
|
|
112
|
+
|
|
113
|
+
afterAll(() => {
|
|
114
|
+
globalThis.fetch = originalFetch;
|
|
115
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Slack channel config handler', () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
secureKeyStore = {};
|
|
121
|
+
credentialMetadataStore = [];
|
|
122
|
+
globalThis.fetch = originalFetch;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('GET returns correct shape when not configured', () => {
|
|
126
|
+
const result = getSlackChannelConfig();
|
|
127
|
+
expect(result.success).toBe(true);
|
|
128
|
+
expect(result.hasBotToken).toBe(false);
|
|
129
|
+
expect(result.hasAppToken).toBe(false);
|
|
130
|
+
expect(result.connected).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('GET returns connected: true when both tokens are set', () => {
|
|
134
|
+
secureKeyStore['credential:slack_channel:bot_token'] = 'xoxb-test';
|
|
135
|
+
secureKeyStore['credential:slack_channel:app_token'] = 'xapp-test';
|
|
136
|
+
|
|
137
|
+
const result = getSlackChannelConfig();
|
|
138
|
+
expect(result.success).toBe(true);
|
|
139
|
+
expect(result.hasBotToken).toBe(true);
|
|
140
|
+
expect(result.hasAppToken).toBe(true);
|
|
141
|
+
expect(result.connected).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('GET returns metadata when available', () => {
|
|
145
|
+
secureKeyStore['credential:slack_channel:bot_token'] = 'xoxb-test';
|
|
146
|
+
credentialMetadataStore.push({
|
|
147
|
+
service: 'slack_channel',
|
|
148
|
+
field: 'bot_token',
|
|
149
|
+
accountInfo: JSON.stringify({
|
|
150
|
+
teamId: 'T123',
|
|
151
|
+
teamName: 'TestTeam',
|
|
152
|
+
botUserId: 'U_BOT',
|
|
153
|
+
botUsername: 'testbot',
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = getSlackChannelConfig();
|
|
158
|
+
expect(result.teamId).toBe('T123');
|
|
159
|
+
expect(result.teamName).toBe('TestTeam');
|
|
160
|
+
expect(result.botUserId).toBe('U_BOT');
|
|
161
|
+
expect(result.botUsername).toBe('testbot');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('POST validates app token shape (xapp- prefix required)', async () => {
|
|
165
|
+
const result = await setSlackChannelConfig(undefined, 'invalid-token');
|
|
166
|
+
expect(result.success).toBe(false);
|
|
167
|
+
expect(result.error).toContain('xapp-');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('POST accepts valid app token with xapp- prefix', async () => {
|
|
171
|
+
const result = await setSlackChannelConfig(undefined, 'xapp-valid-token-123');
|
|
172
|
+
expect(result.success).toBe(true);
|
|
173
|
+
expect(result.hasAppToken).toBe(true);
|
|
174
|
+
expect(secureKeyStore['credential:slack_channel:app_token']).toBe('xapp-valid-token-123');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('POST validates bot token via Slack auth.test API', async () => {
|
|
178
|
+
globalThis.fetch = (async () => {
|
|
179
|
+
return new Response(JSON.stringify({
|
|
180
|
+
ok: true,
|
|
181
|
+
team_id: 'T_TEAM',
|
|
182
|
+
team: 'MyTeam',
|
|
183
|
+
user_id: 'U_BOT',
|
|
184
|
+
user: 'mybot',
|
|
185
|
+
}), {
|
|
186
|
+
status: 200,
|
|
187
|
+
headers: { 'content-type': 'application/json' },
|
|
188
|
+
});
|
|
189
|
+
}) as typeof globalThis.fetch;
|
|
190
|
+
|
|
191
|
+
const result = await setSlackChannelConfig('xoxb-valid-bot-token');
|
|
192
|
+
expect(result.success).toBe(true);
|
|
193
|
+
expect(result.hasBotToken).toBe(true);
|
|
194
|
+
expect(result.teamId).toBe('T_TEAM');
|
|
195
|
+
expect(result.teamName).toBe('MyTeam');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('POST returns error when Slack auth.test rejects bot token', async () => {
|
|
199
|
+
globalThis.fetch = (async () => {
|
|
200
|
+
return new Response(JSON.stringify({
|
|
201
|
+
ok: false,
|
|
202
|
+
error: 'invalid_auth',
|
|
203
|
+
}), {
|
|
204
|
+
status: 200,
|
|
205
|
+
headers: { 'content-type': 'application/json' },
|
|
206
|
+
});
|
|
207
|
+
}) as typeof globalThis.fetch;
|
|
208
|
+
|
|
209
|
+
const result = await setSlackChannelConfig('xoxb-bad-token');
|
|
210
|
+
expect(result.success).toBe(false);
|
|
211
|
+
expect(result.error).toContain('invalid_auth');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('DELETE clears credentials', () => {
|
|
215
|
+
secureKeyStore['credential:slack_channel:bot_token'] = 'xoxb-test';
|
|
216
|
+
secureKeyStore['credential:slack_channel:app_token'] = 'xapp-test';
|
|
217
|
+
credentialMetadataStore.push({ service: 'slack_channel', field: 'bot_token' });
|
|
218
|
+
credentialMetadataStore.push({ service: 'slack_channel', field: 'app_token' });
|
|
219
|
+
|
|
220
|
+
const result = clearSlackChannelConfig();
|
|
221
|
+
expect(result.success).toBe(true);
|
|
222
|
+
expect(result.hasBotToken).toBe(false);
|
|
223
|
+
expect(result.hasAppToken).toBe(false);
|
|
224
|
+
expect(result.connected).toBe(false);
|
|
225
|
+
|
|
226
|
+
expect(secureKeyStore['credential:slack_channel:bot_token']).toBeUndefined();
|
|
227
|
+
expect(secureKeyStore['credential:slack_channel:app_token']).toBeUndefined();
|
|
228
|
+
expect(credentialMetadataStore).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -393,13 +393,13 @@ describe('SubagentManager abort race guard', () => {
|
|
|
393
393
|
});
|
|
394
394
|
|
|
395
395
|
describe('SubagentManager sendMessage validation', () => {
|
|
396
|
-
test('rejects empty content without throwing', () => {
|
|
396
|
+
test('rejects empty content without throwing', async () => {
|
|
397
397
|
const manager = new SubagentManager();
|
|
398
398
|
const subagentId = 'sub-1';
|
|
399
399
|
injectFakeSubagent(manager, subagentId, makeState(subagentId));
|
|
400
400
|
|
|
401
|
-
expect(manager.sendMessage(subagentId, '')).toBe('empty');
|
|
402
|
-
expect(manager.sendMessage(subagentId, ' ')).toBe('empty');
|
|
403
|
-
expect(manager.sendMessage(subagentId, '\n\t')).toBe('empty');
|
|
401
|
+
expect(await manager.sendMessage(subagentId, '')).toBe('empty');
|
|
402
|
+
expect(await manager.sendMessage(subagentId, ' ')).toBe('empty');
|
|
403
|
+
expect(await manager.sendMessage(subagentId, '\n\t')).toBe('empty');
|
|
404
404
|
});
|
|
405
405
|
});
|