@vellumai/assistant 0.3.15 → 0.3.18
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 +211 -12
- package/Dockerfile +1 -1
- package/README.md +11 -5
- package/docs/architecture/http-token-refresh.md +274 -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 +328 -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 +19 -15
- package/src/__tests__/checker.test.ts +103 -48
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +356 -0
- package/src/__tests__/conversation-pairing.test.ts +127 -27
- 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 +425 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-store.test.ts +182 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-dispatch.test.ts +120 -0
- package/src/__tests__/guardian-outbound-http.test.ts +194 -2
- 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 +23 -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 +281 -0
- package/src/__tests__/notification-broadcaster.test.ts +115 -4
- package/src/__tests__/notification-decision-strategy.test.ts +138 -1
- package/src/__tests__/notification-deep-link.test.ts +44 -1
- package/src/__tests__/notification-guardian-path.test.ts +157 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- 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 +38 -22
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +405 -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 +323 -0
- package/src/__tests__/update-template-contract.test.ts +24 -0
- package/src/__tests__/voice-session-bridge.test.ts +109 -9
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +241 -39
- 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/guardian-dispatch.ts +8 -0
- 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 +8 -6
- package/src/cli/core-commands.ts +43 -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 +15 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +54 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin-template-path.ts +6 -0
- package/src/config/update-bulletin.ts +97 -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 +4 -2
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +219 -8
- 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 +2 -2
- 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/identity.ts +45 -25
- package/src/daemon/handlers/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- 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/workspace.ts +12 -1
- package/src/daemon/ipc-contract-inventory.json +6 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +314 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +31 -9
- 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 +546 -59
- 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 +5 -6
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +60 -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 +35 -567
- package/src/memory/channel-guardian-store.ts +63 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +0 -3
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +44 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +33 -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 +136 -0
- package/src/memory/guardian-action-store.ts +418 -5
- 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 +521 -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/032-guardian-delivery-conversation-index.ts +15 -0
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -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 +10 -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 +4 -0
- package/src/memory/schema.ts +31 -8
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +159 -18
- package/src/notifications/broadcaster.ts +69 -33
- package/src/notifications/conversation-pairing.ts +99 -21
- package/src/notifications/decision-engine.ts +176 -8
- package/src/notifications/deliveries-store.ts +39 -8
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +269 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/checker.ts +1 -16
- package/src/permissions/defaults.ts +25 -5
- 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 +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- 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 +2 -1
- 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 +78 -16
- 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 +1 -1
- 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 +271 -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/version.ts +29 -2
- 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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for the guardian-verify-setup skill.
|
|
3
|
+
*
|
|
4
|
+
* Ensures the voice verification flow includes proactive auto-check polling
|
|
5
|
+
* so the user does not have to manually ask whether verification succeeded.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from 'bun:test';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Locate the skill SKILL.md
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const ASSISTANT_DIR = resolve(import.meta.dirname ?? __dirname, '..', '..');
|
|
18
|
+
const SKILL_PATH = resolve(
|
|
19
|
+
ASSISTANT_DIR,
|
|
20
|
+
'src',
|
|
21
|
+
'config',
|
|
22
|
+
'vellum-skills',
|
|
23
|
+
'guardian-verify-setup',
|
|
24
|
+
'SKILL.md',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const skillContent = readFileSync(SKILL_PATH, 'utf-8');
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Tests
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
describe('guardian-verify-setup skill — voice auto-followup', () => {
|
|
34
|
+
test('voice path in Step 3 references the auto-check polling loop', () => {
|
|
35
|
+
// The voice success instruction in Step 3 must direct the assistant to
|
|
36
|
+
// begin the polling loop rather than waiting for the user to report back.
|
|
37
|
+
expect(skillContent).toContain(
|
|
38
|
+
'immediately begin the voice auto-check polling loop',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('voice path in Step 4 (resend) references the auto-check polling loop', () => {
|
|
43
|
+
// After a voice resend, the same auto-check behavior must kick in.
|
|
44
|
+
const resendSection = skillContent.split('## Step 4')[1]?.split('## Step 5')[0] ?? '';
|
|
45
|
+
expect(resendSection).toContain(
|
|
46
|
+
'voice auto-check polling loop',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('contains a Voice Auto-Check Polling section', () => {
|
|
51
|
+
expect(skillContent).toContain('## Voice Auto-Check Polling');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('polling section specifies the correct status endpoint for voice', () => {
|
|
55
|
+
const pollingSection =
|
|
56
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
57
|
+
expect(pollingSection).toContain(
|
|
58
|
+
'/v1/integrations/guardian/status?channel=voice',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('polling section includes ~15 second interval', () => {
|
|
63
|
+
const pollingSection =
|
|
64
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
65
|
+
expect(pollingSection).toContain('~15 seconds');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('polling section includes 2-minute timeout', () => {
|
|
69
|
+
const pollingSection =
|
|
70
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
71
|
+
expect(pollingSection).toContain('2 minutes');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('polling section checks for bound: true', () => {
|
|
75
|
+
const pollingSection =
|
|
76
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
77
|
+
expect(pollingSection).toContain('bound: true');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('polling section includes proactive success confirmation', () => {
|
|
81
|
+
const pollingSection =
|
|
82
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
83
|
+
expect(pollingSection).toContain('proactive success message');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('polling section includes timeout fallback with resend/restart offer', () => {
|
|
87
|
+
const pollingSection =
|
|
88
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
89
|
+
expect(pollingSection).toContain('timeout');
|
|
90
|
+
expect(pollingSection).toContain('resend');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('polling section includes rebind guard against false-success from pre-existing binding', () => {
|
|
94
|
+
const pollingSection =
|
|
95
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
96
|
+
// Must mention rebind guard concept
|
|
97
|
+
expect(pollingSection).toContain('Rebind guard');
|
|
98
|
+
// Must instruct not to trust the first bound: true in a rebind flow
|
|
99
|
+
expect(pollingSection).toContain(
|
|
100
|
+
'do NOT treat the first `bound: true` poll result as success',
|
|
101
|
+
);
|
|
102
|
+
// Must reference bound_at timestamp comparison as the primary mechanism
|
|
103
|
+
expect(pollingSection).toContain('bound_at');
|
|
104
|
+
// Must have a fallback for when bound_at is unavailable
|
|
105
|
+
expect(pollingSection).toContain('second poll onward');
|
|
106
|
+
// Must clarify non-rebind flows are unaffected
|
|
107
|
+
expect(pollingSection).toContain('Non-rebind flows');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('polling is voice-only — does not apply to SMS or Telegram', () => {
|
|
111
|
+
const pollingSection =
|
|
112
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
113
|
+
expect(pollingSection).toContain('voice-only');
|
|
114
|
+
expect(pollingSection).toContain('Do NOT poll for SMS or Telegram');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('no instruction requires waiting for user to ask "did it work?"', () => {
|
|
118
|
+
// The skill should never instruct the assistant to wait for the user to
|
|
119
|
+
// confirm that voice verification worked. The auto-check polling loop
|
|
120
|
+
// makes this unnecessary.
|
|
121
|
+
const voiceAutoCheckSection =
|
|
122
|
+
skillContent.split('## Voice Auto-Check Polling')[1]?.split('## Step 6')[0] ?? '';
|
|
123
|
+
expect(voiceAutoCheckSection).toContain(
|
|
124
|
+
'Do NOT require the user to ask',
|
|
125
|
+
);
|
|
126
|
+
// The voice bullet in Step 3 should not instruct the assistant to wait
|
|
127
|
+
// for the user to confirm or ask if it worked. Narrow to just the voice
|
|
128
|
+
// bullet line to avoid false positives from Telegram's "wait for the
|
|
129
|
+
// user to confirm they clicked the link" which is unrelated to voice.
|
|
130
|
+
const step3Section = skillContent
|
|
131
|
+
.split('## Step 3')[1]
|
|
132
|
+
?.split('## Step 4')[0] ?? '';
|
|
133
|
+
const voiceBullet = step3Section
|
|
134
|
+
.split('\n')
|
|
135
|
+
.filter((line) => /^\s*-\s+\*\*Voice\*\*/.test(line))
|
|
136
|
+
.join('\n');
|
|
137
|
+
expect(voiceBullet).not.toHaveLength(0);
|
|
138
|
+
expect(voiceBullet).not.toContain('wait for the user to confirm');
|
|
139
|
+
expect(voiceBullet).not.toContain('ask the user if it worked');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -785,7 +785,6 @@ describe('Telegram config handler', () => {
|
|
|
785
785
|
expect((setCommandsBody as { commands: Array<{ command: string; description: string }> }).commands).toEqual([
|
|
786
786
|
{ command: 'new', description: 'Start a new conversation' },
|
|
787
787
|
{ command: 'help', description: 'Show available commands' },
|
|
788
|
-
{ command: 'guardian_verify', description: 'Verify your guardian identity' },
|
|
789
788
|
]);
|
|
790
789
|
});
|
|
791
790
|
|
|
@@ -865,7 +864,7 @@ describe('Telegram config handler', () => {
|
|
|
865
864
|
expect(res.error).toContain('Failed to set bot commands');
|
|
866
865
|
});
|
|
867
866
|
|
|
868
|
-
test('default command registration includes /new
|
|
867
|
+
test('default command registration includes /new and /help', async () => {
|
|
869
868
|
secureKeyStore['credential:telegram:bot_token'] = 'test-bot-token';
|
|
870
869
|
secureKeyStore['credential:telegram:webhook_secret'] = 'test-webhook-secret';
|
|
871
870
|
|
|
@@ -893,11 +892,10 @@ describe('Telegram config handler', () => {
|
|
|
893
892
|
expect(res.success).toBe(true);
|
|
894
893
|
|
|
895
894
|
const commands = (setCommandsBody as { commands: Array<{ command: string; description: string }> }).commands;
|
|
896
|
-
expect(commands).toHaveLength(
|
|
895
|
+
expect(commands).toHaveLength(2);
|
|
897
896
|
expect(commands).toEqual([
|
|
898
897
|
{ command: 'new', description: 'Start a new conversation' },
|
|
899
898
|
{ command: 'help', description: 'Show available commands' },
|
|
900
|
-
{ command: 'guardian_verify', description: 'Verify your guardian identity' },
|
|
901
899
|
]);
|
|
902
900
|
});
|
|
903
901
|
});
|
|
@@ -950,7 +948,8 @@ describe('Guardian verification IPC actions', () => {
|
|
|
950
948
|
expect(res.success).toBe(true);
|
|
951
949
|
expect(res.secret).toBeDefined();
|
|
952
950
|
expect(res.instruction).toBeDefined();
|
|
953
|
-
expect(res.instruction).toContain('
|
|
951
|
+
expect(res.instruction).toContain('send');
|
|
952
|
+
expect(res.instruction).toContain('code');
|
|
954
953
|
});
|
|
955
954
|
|
|
956
955
|
test('unknown action returns error', () => {
|
|
@@ -985,7 +984,8 @@ describe('Guardian verification IPC actions', () => {
|
|
|
985
984
|
const res = sent[0] as { type: string; success: boolean; secret?: string; instruction?: string };
|
|
986
985
|
expect(res.success).toBe(true);
|
|
987
986
|
expect(res.secret).toBeDefined();
|
|
988
|
-
expect(res.instruction).toContain('
|
|
987
|
+
expect(res.instruction).toContain('send');
|
|
988
|
+
expect(res.instruction).toContain('code');
|
|
989
989
|
});
|
|
990
990
|
|
|
991
991
|
test('status action with explicit assistantId checks binding for that assistant', () => {
|
|
@@ -135,10 +135,19 @@ describe('Hook Runner', () => {
|
|
|
135
135
|
expect(wsDir).toStartWith(rootDir);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
//
|
|
139
|
-
// script is a bash process tree — SIGTERM/SIGKILL may not trigger 'close'
|
|
140
|
-
// causing the test to hang. The timeout logic
|
|
141
|
-
//
|
|
138
|
+
// SKIPPED: child.kill() + 'close' event is unreliable on macOS when the hook
|
|
139
|
+
// script is a bash process tree — SIGTERM/SIGKILL may not trigger the 'close'
|
|
140
|
+
// event, causing the test to hang indefinitely. The timeout logic in runner.ts
|
|
141
|
+
// (lines 90-102) works correctly in production, but the combination of
|
|
142
|
+
// `sleep 10` in a child bash process + kill + waiting for 'close' is not
|
|
143
|
+
// deterministically testable in unit tests on macOS.
|
|
144
|
+
//
|
|
145
|
+
// Re-enable when:
|
|
146
|
+
// - Bun's child_process reliably emits 'close' after SIGKILL on macOS, OR
|
|
147
|
+
// - The runner is refactored to use Bun.spawn() (which has different process
|
|
148
|
+
// group semantics), OR
|
|
149
|
+
// - A test-only flag is added to use a more controllable timeout mechanism
|
|
150
|
+
// (e.g., AbortController) instead of child.kill().
|
|
142
151
|
test.skip('[experimental] times out after specified duration', async () => {
|
|
143
152
|
const hook = createTestHook(hooksDir, 'slow-hook', '#!/bin/bash\nsleep 10');
|
|
144
153
|
const eventData: HookEventData = { event: 'pre-tool-execute' };
|
|
@@ -0,0 +1,443 @@
|
|
|
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(), 'ingress-routes-http-test-'));
|
|
8
|
+
|
|
9
|
+
mock.module('../util/platform.js', () => ({
|
|
10
|
+
getDataDir: () => testDir,
|
|
11
|
+
isMacOS: () => process.platform === 'darwin',
|
|
12
|
+
isLinux: () => process.platform === 'linux',
|
|
13
|
+
isWindows: () => process.platform === 'win32',
|
|
14
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
15
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
16
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
17
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
18
|
+
ensureDataDir: () => {},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
mock.module('../util/logger.js', () => ({
|
|
22
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
23
|
+
get: () => () => {},
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { getSqlite, initializeDb, resetDb } from '../memory/db.js';
|
|
28
|
+
import {
|
|
29
|
+
handleBlockMember,
|
|
30
|
+
handleCreateInvite,
|
|
31
|
+
handleListInvites,
|
|
32
|
+
handleListMembers,
|
|
33
|
+
handleRedeemInvite,
|
|
34
|
+
handleRevokeInvite,
|
|
35
|
+
handleRevokeMember,
|
|
36
|
+
handleUpsertMember,
|
|
37
|
+
} from '../runtime/routes/ingress-routes.js';
|
|
38
|
+
|
|
39
|
+
initializeDb();
|
|
40
|
+
|
|
41
|
+
afterAll(() => {
|
|
42
|
+
resetDb();
|
|
43
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function resetTables() {
|
|
47
|
+
getSqlite().run('DELETE FROM assistant_ingress_members');
|
|
48
|
+
getSqlite().run('DELETE FROM assistant_ingress_invites');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Member routes
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
describe('ingress member HTTP routes', () => {
|
|
56
|
+
beforeEach(resetTables);
|
|
57
|
+
|
|
58
|
+
test('POST /v1/ingress/members — upsert creates a member', async () => {
|
|
59
|
+
const req = new Request('http://localhost/v1/ingress/members', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
sourceChannel: 'telegram',
|
|
64
|
+
externalUserId: 'user-1',
|
|
65
|
+
displayName: 'Test User',
|
|
66
|
+
policy: 'allow',
|
|
67
|
+
status: 'active',
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const res = await handleUpsertMember(req);
|
|
72
|
+
const body = await res.json() as Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
expect(res.status).toBe(200);
|
|
75
|
+
expect(body.ok).toBe(true);
|
|
76
|
+
expect(body.member).toBeDefined();
|
|
77
|
+
const member = body.member as Record<string, unknown>;
|
|
78
|
+
expect(member.sourceChannel).toBe('telegram');
|
|
79
|
+
expect(member.externalUserId).toBe('user-1');
|
|
80
|
+
expect(member.displayName).toBe('Test User');
|
|
81
|
+
expect(member.policy).toBe('allow');
|
|
82
|
+
expect(member.status).toBe('active');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('POST /v1/ingress/members — missing sourceChannel returns 400', async () => {
|
|
86
|
+
const req = new Request('http://localhost/v1/ingress/members', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
externalUserId: 'user-1',
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const res = await handleUpsertMember(req);
|
|
95
|
+
const body = await res.json() as Record<string, unknown>;
|
|
96
|
+
|
|
97
|
+
expect(res.status).toBe(400);
|
|
98
|
+
expect(body.ok).toBe(false);
|
|
99
|
+
expect(body.error).toContain('sourceChannel');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('POST /v1/ingress/members — missing identity returns 400', async () => {
|
|
103
|
+
const req = new Request('http://localhost/v1/ingress/members', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
sourceChannel: 'telegram',
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const res = await handleUpsertMember(req);
|
|
112
|
+
const body = await res.json() as Record<string, unknown>;
|
|
113
|
+
|
|
114
|
+
expect(res.status).toBe(400);
|
|
115
|
+
expect(body.ok).toBe(false);
|
|
116
|
+
expect(body.error).toContain('externalUserId');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('GET /v1/ingress/members — lists members', async () => {
|
|
120
|
+
// Create two members
|
|
121
|
+
await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ sourceChannel: 'telegram', externalUserId: 'user-1', status: 'active' }),
|
|
125
|
+
}));
|
|
126
|
+
await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body: JSON.stringify({ sourceChannel: 'telegram', externalUserId: 'user-2', status: 'active' }),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const url = new URL('http://localhost/v1/ingress/members');
|
|
133
|
+
const res = handleListMembers(url);
|
|
134
|
+
const body = await res.json() as Record<string, unknown>;
|
|
135
|
+
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
expect(body.ok).toBe(true);
|
|
138
|
+
expect(Array.isArray(body.members)).toBe(true);
|
|
139
|
+
expect((body.members as unknown[]).length).toBe(2);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('GET /v1/ingress/members — filters by sourceChannel', async () => {
|
|
143
|
+
await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
146
|
+
body: JSON.stringify({ sourceChannel: 'telegram', externalUserId: 'user-1', status: 'active' }),
|
|
147
|
+
}));
|
|
148
|
+
await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({ sourceChannel: 'sms', externalUserId: 'user-2', status: 'active' }),
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
const url = new URL('http://localhost/v1/ingress/members?sourceChannel=telegram');
|
|
155
|
+
const res = handleListMembers(url);
|
|
156
|
+
const body = await res.json() as Record<string, unknown>;
|
|
157
|
+
|
|
158
|
+
expect((body.members as unknown[]).length).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('DELETE /v1/ingress/members/:id — revokes a member', async () => {
|
|
162
|
+
const createRes = await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({ sourceChannel: 'telegram', externalUserId: 'user-1', status: 'active' }),
|
|
166
|
+
}));
|
|
167
|
+
const created = await createRes.json() as { member: { id: string } };
|
|
168
|
+
|
|
169
|
+
const req = new Request('http://localhost/v1/ingress/members/' + created.member.id, {
|
|
170
|
+
method: 'DELETE',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ reason: 'test revoke' }),
|
|
173
|
+
});
|
|
174
|
+
const res = await handleRevokeMember(req, created.member.id);
|
|
175
|
+
const body = await res.json() as Record<string, unknown>;
|
|
176
|
+
|
|
177
|
+
expect(res.status).toBe(200);
|
|
178
|
+
expect(body.ok).toBe(true);
|
|
179
|
+
const member = body.member as Record<string, unknown>;
|
|
180
|
+
expect(member.status).toBe('revoked');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('DELETE /v1/ingress/members/:id — not found returns 404', async () => {
|
|
184
|
+
const req = new Request('http://localhost/v1/ingress/members/nonexistent', {
|
|
185
|
+
method: 'DELETE',
|
|
186
|
+
});
|
|
187
|
+
const res = await handleRevokeMember(req, 'nonexistent');
|
|
188
|
+
const body = await res.json() as Record<string, unknown>;
|
|
189
|
+
|
|
190
|
+
expect(res.status).toBe(404);
|
|
191
|
+
expect(body.ok).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('POST /v1/ingress/members/:id/block — blocks a member', async () => {
|
|
195
|
+
const createRes = await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'Content-Type': 'application/json' },
|
|
198
|
+
body: JSON.stringify({ sourceChannel: 'telegram', externalUserId: 'user-1', status: 'active' }),
|
|
199
|
+
}));
|
|
200
|
+
const created = await createRes.json() as { member: { id: string } };
|
|
201
|
+
|
|
202
|
+
const req = new Request('http://localhost/v1/ingress/members/' + created.member.id + '/block', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({ reason: 'spam' }),
|
|
206
|
+
});
|
|
207
|
+
const res = await handleBlockMember(req, created.member.id);
|
|
208
|
+
const body = await res.json() as Record<string, unknown>;
|
|
209
|
+
|
|
210
|
+
expect(res.status).toBe(200);
|
|
211
|
+
expect(body.ok).toBe(true);
|
|
212
|
+
const member = body.member as Record<string, unknown>;
|
|
213
|
+
expect(member.status).toBe('blocked');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('POST /v1/ingress/members/:id/block — already blocked returns 404', async () => {
|
|
217
|
+
const createRes = await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
220
|
+
body: JSON.stringify({ sourceChannel: 'telegram', externalUserId: 'user-1', status: 'active' }),
|
|
221
|
+
}));
|
|
222
|
+
const created = await createRes.json() as { member: { id: string } };
|
|
223
|
+
|
|
224
|
+
// Block first time
|
|
225
|
+
await handleBlockMember(
|
|
226
|
+
new Request('http://localhost/block', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify({}),
|
|
230
|
+
}),
|
|
231
|
+
created.member.id,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Block second time
|
|
235
|
+
const req = new Request('http://localhost/block', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: { 'Content-Type': 'application/json' },
|
|
238
|
+
body: JSON.stringify({}),
|
|
239
|
+
});
|
|
240
|
+
const res = await handleBlockMember(req, created.member.id);
|
|
241
|
+
const body = await res.json() as Record<string, unknown>;
|
|
242
|
+
|
|
243
|
+
expect(res.status).toBe(404);
|
|
244
|
+
expect(body.ok).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Invite routes
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
describe('ingress invite HTTP routes', () => {
|
|
253
|
+
beforeEach(resetTables);
|
|
254
|
+
|
|
255
|
+
test('POST /v1/ingress/invites — creates an invite', async () => {
|
|
256
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
sourceChannel: 'telegram',
|
|
261
|
+
note: 'Test invite',
|
|
262
|
+
maxUses: 5,
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const res = await handleCreateInvite(req);
|
|
267
|
+
const body = await res.json() as Record<string, unknown>;
|
|
268
|
+
|
|
269
|
+
expect(res.status).toBe(201);
|
|
270
|
+
expect(body.ok).toBe(true);
|
|
271
|
+
const invite = body.invite as Record<string, unknown>;
|
|
272
|
+
expect(invite.sourceChannel).toBe('telegram');
|
|
273
|
+
expect(invite.note).toBe('Test invite');
|
|
274
|
+
expect(invite.maxUses).toBe(5);
|
|
275
|
+
expect(invite.status).toBe('active');
|
|
276
|
+
// Raw token should be returned on create
|
|
277
|
+
expect(typeof invite.token).toBe('string');
|
|
278
|
+
expect((invite.token as string).length).toBeGreaterThan(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('POST /v1/ingress/invites — missing sourceChannel returns 400', async () => {
|
|
282
|
+
const req = new Request('http://localhost/v1/ingress/invites', {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
headers: { 'Content-Type': 'application/json' },
|
|
285
|
+
body: JSON.stringify({ note: 'No channel' }),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const res = await handleCreateInvite(req);
|
|
289
|
+
const body = await res.json() as Record<string, unknown>;
|
|
290
|
+
|
|
291
|
+
expect(res.status).toBe(400);
|
|
292
|
+
expect(body.ok).toBe(false);
|
|
293
|
+
expect(body.error).toContain('sourceChannel');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('GET /v1/ingress/invites — lists invites', async () => {
|
|
297
|
+
// Create two invites
|
|
298
|
+
await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
301
|
+
body: JSON.stringify({ sourceChannel: 'telegram' }),
|
|
302
|
+
}));
|
|
303
|
+
await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({ sourceChannel: 'telegram' }),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
const url = new URL('http://localhost/v1/ingress/invites');
|
|
310
|
+
const res = handleListInvites(url);
|
|
311
|
+
const body = await res.json() as Record<string, unknown>;
|
|
312
|
+
|
|
313
|
+
expect(res.status).toBe(200);
|
|
314
|
+
expect(body.ok).toBe(true);
|
|
315
|
+
expect(Array.isArray(body.invites)).toBe(true);
|
|
316
|
+
expect((body.invites as unknown[]).length).toBe(2);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('DELETE /v1/ingress/invites/:id — revokes an invite', async () => {
|
|
320
|
+
const createRes = await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: { 'Content-Type': 'application/json' },
|
|
323
|
+
body: JSON.stringify({ sourceChannel: 'telegram' }),
|
|
324
|
+
}));
|
|
325
|
+
const created = await createRes.json() as { invite: { id: string } };
|
|
326
|
+
|
|
327
|
+
const res = handleRevokeInvite(created.invite.id);
|
|
328
|
+
const body = await res.json() as Record<string, unknown>;
|
|
329
|
+
|
|
330
|
+
expect(res.status).toBe(200);
|
|
331
|
+
expect(body.ok).toBe(true);
|
|
332
|
+
const invite = body.invite as Record<string, unknown>;
|
|
333
|
+
expect(invite.status).toBe('revoked');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('DELETE /v1/ingress/invites/:id — not found returns 404', () => {
|
|
337
|
+
const res = handleRevokeInvite('nonexistent-id');
|
|
338
|
+
expect(res.status).toBe(404);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('POST /v1/ingress/invites/redeem — redeems an invite', async () => {
|
|
342
|
+
// Create an invite first
|
|
343
|
+
const createRes = await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: { 'Content-Type': 'application/json' },
|
|
346
|
+
body: JSON.stringify({ sourceChannel: 'telegram', maxUses: 1 }),
|
|
347
|
+
}));
|
|
348
|
+
const created = await createRes.json() as { invite: { token: string } };
|
|
349
|
+
|
|
350
|
+
const req = new Request('http://localhost/v1/ingress/invites/redeem', {
|
|
351
|
+
method: 'POST',
|
|
352
|
+
headers: { 'Content-Type': 'application/json' },
|
|
353
|
+
body: JSON.stringify({
|
|
354
|
+
token: created.invite.token,
|
|
355
|
+
externalUserId: 'redeemer-1',
|
|
356
|
+
sourceChannel: 'telegram',
|
|
357
|
+
}),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const res = await handleRedeemInvite(req);
|
|
361
|
+
const body = await res.json() as Record<string, unknown>;
|
|
362
|
+
|
|
363
|
+
expect(res.status).toBe(200);
|
|
364
|
+
expect(body.ok).toBe(true);
|
|
365
|
+
const invite = body.invite as Record<string, unknown>;
|
|
366
|
+
expect(invite.useCount).toBe(1);
|
|
367
|
+
// Single-use invite should be fully redeemed
|
|
368
|
+
expect(invite.status).toBe('redeemed');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('POST /v1/ingress/invites/redeem — missing token returns 400', async () => {
|
|
372
|
+
const req = new Request('http://localhost/v1/ingress/invites/redeem', {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
body: JSON.stringify({ externalUserId: 'redeemer-1' }),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const res = await handleRedeemInvite(req);
|
|
379
|
+
const body = await res.json() as Record<string, unknown>;
|
|
380
|
+
|
|
381
|
+
expect(res.status).toBe(400);
|
|
382
|
+
expect(body.ok).toBe(false);
|
|
383
|
+
expect(body.error).toContain('token');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('POST /v1/ingress/invites/redeem — invalid token returns 400', async () => {
|
|
387
|
+
const req = new Request('http://localhost/v1/ingress/invites/redeem', {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
headers: { 'Content-Type': 'application/json' },
|
|
390
|
+
body: JSON.stringify({ token: 'invalid-token' }),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const res = await handleRedeemInvite(req);
|
|
394
|
+
const body = await res.json() as Record<string, unknown>;
|
|
395
|
+
|
|
396
|
+
expect(res.status).toBe(400);
|
|
397
|
+
expect(body.ok).toBe(false);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// IPC backward compatibility — shared logic produces same results
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
describe('ingress service shared logic', () => {
|
|
406
|
+
beforeEach(resetTables);
|
|
407
|
+
|
|
408
|
+
test('member upsert + list round-trip through shared service', async () => {
|
|
409
|
+
const createRes = await handleUpsertMember(new Request('http://localhost/v1/ingress/members', {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: { 'Content-Type': 'application/json' },
|
|
412
|
+
body: JSON.stringify({
|
|
413
|
+
sourceChannel: 'telegram',
|
|
414
|
+
externalUserId: 'user-rt',
|
|
415
|
+
displayName: 'Round Trip',
|
|
416
|
+
policy: 'allow',
|
|
417
|
+
status: 'active',
|
|
418
|
+
}),
|
|
419
|
+
}));
|
|
420
|
+
const created = await createRes.json() as { member: { id: string; displayName: string } };
|
|
421
|
+
expect(created.member.displayName).toBe('Round Trip');
|
|
422
|
+
|
|
423
|
+
const listRes = handleListMembers(new URL('http://localhost/v1/ingress/members'));
|
|
424
|
+
const listed = await listRes.json() as { members: Array<{ id: string; displayName: string }> };
|
|
425
|
+
expect(listed.members.length).toBe(1);
|
|
426
|
+
expect(listed.members[0].id).toBe(created.member.id);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('invite create + revoke round-trip through shared service', async () => {
|
|
430
|
+
const createRes = await handleCreateInvite(new Request('http://localhost/v1/ingress/invites', {
|
|
431
|
+
method: 'POST',
|
|
432
|
+
headers: { 'Content-Type': 'application/json' },
|
|
433
|
+
body: JSON.stringify({ sourceChannel: 'telegram' }),
|
|
434
|
+
}));
|
|
435
|
+
const created = await createRes.json() as { invite: { id: string; status: string } };
|
|
436
|
+
expect(created.invite.status).toBe('active');
|
|
437
|
+
|
|
438
|
+
const revokeRes = handleRevokeInvite(created.invite.id);
|
|
439
|
+
const revoked = await revokeRes.json() as { invite: { id: string; status: string } };
|
|
440
|
+
expect(revoked.invite.status).toBe('revoked');
|
|
441
|
+
expect(revoked.invite.id).toBe(created.invite.id);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
@@ -312,4 +312,18 @@ describe('Guardian verification routing section in system prompt', () => {
|
|
|
312
312
|
// Must advise not to re-ask channel if already specified
|
|
313
313
|
expect(lower.includes('do not re-ask') || lower.includes('already specified')).toBe(true);
|
|
314
314
|
});
|
|
315
|
+
|
|
316
|
+
test('routing section disambiguates "set myself up as your guardian" phrasing', () => {
|
|
317
|
+
const prompt = buildSystemPrompt();
|
|
318
|
+
const lower = prompt.toLowerCase();
|
|
319
|
+
expect(lower).toContain('help me set myself up as your guardian');
|
|
320
|
+
expect(lower).toContain('asking to verify themselves as guardian');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('routing section discourages conceptual detours for direct setup requests', () => {
|
|
324
|
+
const prompt = buildSystemPrompt();
|
|
325
|
+
const lower = prompt.toLowerCase();
|
|
326
|
+
expect(lower).toContain('do not give conceptual');
|
|
327
|
+
expect(lower).toContain('unless the user explicitly asks');
|
|
328
|
+
});
|
|
315
329
|
});
|