@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.
Files changed (306) hide show
  1. package/ARCHITECTURE.md +211 -12
  2. package/Dockerfile +1 -1
  3. package/README.md +11 -5
  4. package/docs/architecture/http-token-refresh.md +274 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +328 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +19 -15
  22. package/src/__tests__/checker.test.ts +103 -48
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +356 -0
  25. package/src/__tests__/conversation-pairing.test.ts +127 -27
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-store.test.ts +182 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  40. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  41. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  42. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  43. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  44. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  45. package/src/__tests__/hooks-runner.test.ts +13 -4
  46. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  47. package/src/__tests__/intent-routing.test.ts +14 -0
  48. package/src/__tests__/ipc-snapshot.test.ts +23 -5
  49. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  50. package/src/__tests__/memory-regressions.test.ts +16 -12
  51. package/src/__tests__/non-member-access-request.test.ts +281 -0
  52. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  53. package/src/__tests__/notification-decision-strategy.test.ts +138 -1
  54. package/src/__tests__/notification-deep-link.test.ts +44 -1
  55. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  56. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  57. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  58. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  59. package/src/__tests__/recording-intent.test.ts +1 -0
  60. package/src/__tests__/recording-state-machine.test.ts +328 -17
  61. package/src/__tests__/registry.test.ts +17 -8
  62. package/src/__tests__/relay-server.test.ts +105 -0
  63. package/src/__tests__/reminder.test.ts +13 -0
  64. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  65. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  66. package/src/__tests__/server-history-render.test.ts +8 -8
  67. package/src/__tests__/session-agent-loop.test.ts +1 -0
  68. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -0
  70. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  71. package/src/__tests__/slack-channel-config.test.ts +230 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  73. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  74. package/src/__tests__/system-prompt.test.ts +43 -0
  75. package/src/__tests__/task-management-tools.test.ts +3 -3
  76. package/src/__tests__/task-tools.test.ts +3 -3
  77. package/src/__tests__/trust-store.test.ts +38 -22
  78. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
  79. package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
  80. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  81. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  82. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  83. package/src/__tests__/update-bulletin.test.ts +323 -0
  84. package/src/__tests__/update-template-contract.test.ts +24 -0
  85. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  86. package/src/agent/loop.ts +2 -2
  87. package/src/amazon/client.ts +2 -3
  88. package/src/calls/call-controller.ts +241 -39
  89. package/src/calls/call-conversation-messages.ts +2 -2
  90. package/src/calls/call-domain.ts +10 -3
  91. package/src/calls/call-pointer-messages.ts +17 -5
  92. package/src/calls/guardian-action-sweep.ts +77 -36
  93. package/src/calls/guardian-dispatch.ts +8 -0
  94. package/src/calls/relay-server.ts +51 -12
  95. package/src/calls/twilio-routes.ts +3 -1
  96. package/src/calls/types.ts +1 -1
  97. package/src/calls/voice-session-bridge.ts +8 -6
  98. package/src/cli/core-commands.ts +43 -3
  99. package/src/cli/map.ts +8 -5
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  101. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  102. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  103. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  104. package/src/config/computer-use-prompt.ts +1 -0
  105. package/src/config/core-schema.ts +16 -0
  106. package/src/config/env-registry.ts +1 -0
  107. package/src/config/env.ts +16 -1
  108. package/src/config/memory-schema.ts +5 -0
  109. package/src/config/schema.ts +4 -0
  110. package/src/config/system-prompt.ts +69 -2
  111. package/src/config/templates/BOOTSTRAP.md +1 -1
  112. package/src/config/templates/IDENTITY.md +8 -4
  113. package/src/config/templates/SOUL.md +14 -0
  114. package/src/config/templates/UPDATES.md +15 -0
  115. package/src/config/templates/USER.md +5 -1
  116. package/src/config/types.ts +1 -0
  117. package/src/config/update-bulletin-format.ts +54 -0
  118. package/src/config/update-bulletin-state.ts +49 -0
  119. package/src/config/update-bulletin-template-path.ts +6 -0
  120. package/src/config/update-bulletin.ts +97 -0
  121. package/src/config/vellum-skills/catalog.json +6 -0
  122. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  123. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  124. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  125. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  126. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  127. package/src/context/window-manager.ts +43 -3
  128. package/src/daemon/config-watcher.ts +4 -2
  129. package/src/daemon/connection-policy.ts +21 -1
  130. package/src/daemon/daemon-control.ts +219 -8
  131. package/src/daemon/date-context.ts +174 -1
  132. package/src/daemon/guardian-action-generators.ts +175 -0
  133. package/src/daemon/guardian-verification-intent.ts +120 -0
  134. package/src/daemon/handlers/apps.ts +1 -3
  135. package/src/daemon/handlers/config-channels.ts +2 -2
  136. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  137. package/src/daemon/handlers/config-inbox.ts +55 -159
  138. package/src/daemon/handlers/config-ingress.ts +1 -1
  139. package/src/daemon/handlers/config-integrations.ts +1 -1
  140. package/src/daemon/handlers/config-platform.ts +1 -1
  141. package/src/daemon/handlers/config-scheduling.ts +2 -2
  142. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  143. package/src/daemon/handlers/config-telegram.ts +1 -1
  144. package/src/daemon/handlers/config-twilio.ts +1 -1
  145. package/src/daemon/handlers/config-voice.ts +100 -0
  146. package/src/daemon/handlers/config.ts +3 -0
  147. package/src/daemon/handlers/identity.ts +45 -25
  148. package/src/daemon/handlers/misc.ts +83 -5
  149. package/src/daemon/handlers/navigate-settings.ts +27 -0
  150. package/src/daemon/handlers/recording.ts +270 -144
  151. package/src/daemon/handlers/sessions.ts +100 -17
  152. package/src/daemon/handlers/subagents.ts +3 -3
  153. package/src/daemon/handlers/work-items.ts +10 -7
  154. package/src/daemon/ipc-contract/integrations.ts +9 -1
  155. package/src/daemon/ipc-contract/messages.ts +4 -0
  156. package/src/daemon/ipc-contract/sessions.ts +1 -1
  157. package/src/daemon/ipc-contract/settings.ts +26 -0
  158. package/src/daemon/ipc-contract/shared.ts +2 -0
  159. package/src/daemon/ipc-contract/work-items.ts +1 -7
  160. package/src/daemon/ipc-contract/workspace.ts +12 -1
  161. package/src/daemon/ipc-contract-inventory.json +6 -1
  162. package/src/daemon/ipc-contract.ts +5 -1
  163. package/src/daemon/lifecycle.ts +314 -266
  164. package/src/daemon/recording-intent.ts +0 -41
  165. package/src/daemon/response-tier.ts +2 -2
  166. package/src/daemon/server.ts +31 -9
  167. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  168. package/src/daemon/session-agent-loop.ts +15 -8
  169. package/src/daemon/session-history.ts +3 -2
  170. package/src/daemon/session-media-retry.ts +3 -0
  171. package/src/daemon/session-messaging.ts +38 -4
  172. package/src/daemon/session-notifiers.ts +2 -2
  173. package/src/daemon/session-process.ts +546 -59
  174. package/src/daemon/session-queue-manager.ts +2 -0
  175. package/src/daemon/session-runtime-assembly.ts +39 -0
  176. package/src/daemon/session-skill-tools.ts +13 -4
  177. package/src/daemon/session-tool-setup.ts +5 -6
  178. package/src/daemon/session.ts +19 -8
  179. package/src/daemon/tls-certs.ts +60 -13
  180. package/src/daemon/tool-side-effects.ts +13 -5
  181. package/src/gallery/default-gallery.ts +32 -9
  182. package/src/influencer/client.ts +2 -1
  183. package/src/memory/channel-delivery-store.ts +35 -567
  184. package/src/memory/channel-guardian-store.ts +63 -1317
  185. package/src/memory/conflict-store.ts +4 -4
  186. package/src/memory/conversation-attention-store.ts +0 -3
  187. package/src/memory/conversation-crud.ts +668 -0
  188. package/src/memory/conversation-queries.ts +361 -0
  189. package/src/memory/conversation-store.ts +44 -983
  190. package/src/memory/db-connection.ts +3 -0
  191. package/src/memory/db-init.ts +33 -0
  192. package/src/memory/delivery-channels.ts +175 -0
  193. package/src/memory/delivery-crud.ts +211 -0
  194. package/src/memory/delivery-status.ts +199 -0
  195. package/src/memory/embedding-backend.ts +70 -4
  196. package/src/memory/embedding-local.ts +12 -2
  197. package/src/memory/entity-extractor.ts +3 -8
  198. package/src/memory/fts-reconciler.ts +136 -0
  199. package/src/memory/guardian-action-store.ts +418 -5
  200. package/src/memory/guardian-approvals.ts +569 -0
  201. package/src/memory/guardian-bindings.ts +130 -0
  202. package/src/memory/guardian-rate-limits.ts +196 -0
  203. package/src/memory/guardian-verification.ts +521 -0
  204. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  205. package/src/memory/job-utils.ts +8 -5
  206. package/src/memory/jobs-store.ts +66 -6
  207. package/src/memory/jobs-worker.ts +23 -1
  208. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  209. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  210. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  211. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  212. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  213. package/src/memory/migrations/100-core-tables.ts +1 -1
  214. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  215. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  216. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  217. package/src/memory/migrations/113-late-migrations.ts +1 -1
  218. package/src/memory/migrations/116-messages-fts.ts +13 -0
  219. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  220. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  221. package/src/memory/migrations/index.ts +10 -3
  222. package/src/memory/migrations/validate-migration-state.ts +114 -15
  223. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  224. package/src/memory/retriever.ts +46 -13
  225. package/src/memory/schema-migration.ts +4 -0
  226. package/src/memory/schema.ts +31 -8
  227. package/src/memory/search/semantic.ts +8 -90
  228. package/src/notifications/README.md +159 -18
  229. package/src/notifications/broadcaster.ts +69 -33
  230. package/src/notifications/conversation-pairing.ts +99 -21
  231. package/src/notifications/decision-engine.ts +176 -8
  232. package/src/notifications/deliveries-store.ts +39 -8
  233. package/src/notifications/emit-signal.ts +1 -0
  234. package/src/notifications/preferences-store.ts +7 -7
  235. package/src/notifications/thread-candidates.ts +269 -0
  236. package/src/notifications/types.ts +19 -0
  237. package/src/permissions/checker.ts +1 -16
  238. package/src/permissions/defaults.ts +25 -5
  239. package/src/permissions/prompter.ts +17 -0
  240. package/src/permissions/trust-store.ts +2 -0
  241. package/src/providers/failover.ts +19 -0
  242. package/src/providers/registry.ts +46 -1
  243. package/src/runtime/approval-message-composer.ts +1 -1
  244. package/src/runtime/channel-guardian-service.ts +15 -3
  245. package/src/runtime/channel-retry-sweep.ts +7 -2
  246. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  247. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  248. package/src/runtime/guardian-action-message-composer.ts +245 -0
  249. package/src/runtime/guardian-outbound-actions.ts +26 -6
  250. package/src/runtime/guardian-verification-templates.ts +15 -9
  251. package/src/runtime/http-errors.ts +93 -0
  252. package/src/runtime/http-server.ts +133 -44
  253. package/src/runtime/http-types.ts +53 -0
  254. package/src/runtime/ingress-service.ts +237 -0
  255. package/src/runtime/middleware/error-handler.ts +4 -3
  256. package/src/runtime/middleware/rate-limiter.ts +160 -0
  257. package/src/runtime/middleware/request-logger.ts +71 -0
  258. package/src/runtime/middleware/twilio-validation.ts +7 -6
  259. package/src/runtime/pending-interactions.ts +12 -0
  260. package/src/runtime/routes/access-request-decision.ts +215 -0
  261. package/src/runtime/routes/app-routes.ts +25 -18
  262. package/src/runtime/routes/approval-routes.ts +18 -47
  263. package/src/runtime/routes/attachment-routes.ts +15 -41
  264. package/src/runtime/routes/call-routes.ts +20 -20
  265. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  266. package/src/runtime/routes/contact-routes.ts +4 -9
  267. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  268. package/src/runtime/routes/conversation-routes.ts +26 -57
  269. package/src/runtime/routes/debug-routes.ts +71 -0
  270. package/src/runtime/routes/events-routes.ts +3 -2
  271. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  272. package/src/runtime/routes/identity-routes.ts +14 -10
  273. package/src/runtime/routes/inbound-conversation.ts +3 -2
  274. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  275. package/src/runtime/routes/ingress-routes.ts +174 -0
  276. package/src/runtime/routes/integration-routes.ts +78 -16
  277. package/src/runtime/routes/pairing-routes.ts +11 -10
  278. package/src/runtime/routes/secret-routes.ts +10 -18
  279. package/src/runtime/verification-rate-limiter.ts +83 -0
  280. package/src/schedule/schedule-store.ts +13 -1
  281. package/src/schedule/scheduler.ts +1 -1
  282. package/src/security/secret-ingress.ts +5 -2
  283. package/src/security/secret-scanner.ts +72 -6
  284. package/src/subagent/manager.ts +6 -4
  285. package/src/swarm/plan-validator.ts +4 -1
  286. package/src/tasks/task-runner.ts +3 -1
  287. package/src/tools/browser/api-map.ts +9 -6
  288. package/src/tools/calls/call-start.ts +20 -0
  289. package/src/tools/executor.ts +50 -568
  290. package/src/tools/permission-checker.ts +271 -0
  291. package/src/tools/registry.ts +14 -6
  292. package/src/tools/reminder/reminder-store.ts +7 -7
  293. package/src/tools/reminder/reminder.ts +6 -3
  294. package/src/tools/secret-detection-handler.ts +301 -0
  295. package/src/tools/subagent/message.ts +1 -1
  296. package/src/tools/system/voice-config.ts +62 -0
  297. package/src/tools/tasks/index.ts +3 -3
  298. package/src/tools/tasks/work-item-list.ts +3 -3
  299. package/src/tools/tasks/work-item-update.ts +4 -5
  300. package/src/tools/tool-approval-handler.ts +192 -0
  301. package/src/tools/tool-manifest.ts +2 -0
  302. package/src/version.ts +29 -2
  303. package/src/watcher/watcher-store.ts +9 -9
  304. package/src/work-items/work-item-runner.ts +9 -6
  305. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  306. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Tests for M4: Verification success → trusted contact activation.
3
+ *
4
+ * When a requester successfully verifies their identity (enters the correct
5
+ * 6-digit code from an identity-bound outbound session), the system should:
6
+ * 1. Upsert an active member record in assistant_ingress_members
7
+ * 2. Allow subsequent messages through the ACL check
8
+ * 3. Scope the member correctly (no cross-assistant leakage)
9
+ * 4. Reactivate previously revoked members on re-verification
10
+ * 5. NOT create a guardian binding (trusted contacts are not guardians)
11
+ */
12
+ import { mkdtempSync, rmSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Test isolation: in-memory SQLite via temp directory
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const testDir = mkdtempSync(join(tmpdir(), 'trusted-contact-verify-test-'));
23
+
24
+ mock.module('../util/platform.js', () => ({
25
+ getRootDir: () => testDir,
26
+ getDataDir: () => testDir,
27
+ isMacOS: () => process.platform === 'darwin',
28
+ isLinux: () => process.platform === 'linux',
29
+ isWindows: () => process.platform === 'win32',
30
+ getSocketPath: () => join(testDir, 'test.sock'),
31
+ getPidPath: () => join(testDir, 'test.pid'),
32
+ getDbPath: () => join(testDir, 'test.db'),
33
+ getLogPath: () => join(testDir, 'test.log'),
34
+ ensureDataDir: () => {},
35
+ normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
36
+ readHttpToken: () => 'test-bearer-token',
37
+ }));
38
+
39
+ mock.module('../util/logger.js', () => ({
40
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
41
+ get: () => () => {},
42
+ }),
43
+ }));
44
+
45
+ import {
46
+ createBinding,
47
+ getActiveBinding,
48
+ } from '../memory/channel-guardian-store.js';
49
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
50
+ import {
51
+ findMember,
52
+ revokeMember,
53
+ upsertMember,
54
+ } from '../memory/ingress-member-store.js';
55
+ import {
56
+ createOutboundSession,
57
+ validateAndConsumeChallenge,
58
+ } from '../runtime/channel-guardian-service.js';
59
+
60
+ initializeDb();
61
+
62
+ afterAll(() => {
63
+ resetDb();
64
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function resetTables(): void {
72
+ const db = getDb();
73
+ db.run('DELETE FROM channel_guardian_verification_challenges');
74
+ db.run('DELETE FROM channel_guardian_bindings');
75
+ db.run('DELETE FROM channel_guardian_rate_limits');
76
+ db.run('DELETE FROM assistant_ingress_members');
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Tests
81
+ // ---------------------------------------------------------------------------
82
+
83
+ describe('trusted contact verification → member activation', () => {
84
+ beforeEach(() => {
85
+ resetTables();
86
+ });
87
+
88
+ test('successful verification creates active member with allow policy', () => {
89
+ // Simulate M3: guardian approves, outbound session created for the requester
90
+ const session = createOutboundSession({
91
+ assistantId: 'self',
92
+ channel: 'telegram',
93
+ expectedExternalUserId: 'requester-user-123',
94
+ expectedChatId: 'requester-chat-123',
95
+ identityBindingStatus: 'bound',
96
+ destinationAddress: 'requester-chat-123',
97
+ verificationPurpose: 'trusted_contact',
98
+ });
99
+
100
+ // Requester enters the 6-digit code
101
+ const result = validateAndConsumeChallenge(
102
+ 'self',
103
+ 'telegram',
104
+ session.secret,
105
+ 'requester-user-123',
106
+ 'requester-chat-123',
107
+ 'requester_username',
108
+ 'Requester Name',
109
+ );
110
+
111
+ expect(result.success).toBe(true);
112
+ if (result.success) {
113
+ expect(result.verificationType).toBe('trusted_contact');
114
+ }
115
+
116
+ // Simulate the member upsert that inbound-message-handler performs on success
117
+ upsertMember({
118
+ assistantId: 'self',
119
+ sourceChannel: 'telegram',
120
+ externalUserId: 'requester-user-123',
121
+ externalChatId: 'requester-chat-123',
122
+ status: 'active',
123
+ policy: 'allow',
124
+ displayName: 'Requester Name',
125
+ username: 'requester_username',
126
+ });
127
+
128
+ // Verify: active member record exists
129
+ const member = findMember({
130
+ assistantId: 'self',
131
+ sourceChannel: 'telegram',
132
+ externalUserId: 'requester-user-123',
133
+ });
134
+
135
+ expect(member).not.toBeNull();
136
+ expect(member!.status).toBe('active');
137
+ expect(member!.policy).toBe('allow');
138
+ expect(member!.externalUserId).toBe('requester-user-123');
139
+ expect(member!.externalChatId).toBe('requester-chat-123');
140
+ expect(member!.displayName).toBe('Requester Name');
141
+ expect(member!.username).toBe('requester_username');
142
+ expect(member!.assistantId).toBe('self');
143
+ expect(member!.sourceChannel).toBe('telegram');
144
+ });
145
+
146
+ test('post-verify message is accepted (ACL check passes)', () => {
147
+ // Create and verify a trusted contact
148
+ const session = createOutboundSession({
149
+ assistantId: 'self',
150
+ channel: 'telegram',
151
+ expectedExternalUserId: 'requester-user-456',
152
+ expectedChatId: 'requester-chat-456',
153
+ identityBindingStatus: 'bound',
154
+ destinationAddress: 'requester-chat-456',
155
+ verificationPurpose: 'trusted_contact',
156
+ });
157
+
158
+ validateAndConsumeChallenge(
159
+ 'self', 'telegram', session.secret,
160
+ 'requester-user-456', 'requester-chat-456',
161
+ );
162
+
163
+ // Simulate member upsert on verification success
164
+ upsertMember({
165
+ assistantId: 'self',
166
+ sourceChannel: 'telegram',
167
+ externalUserId: 'requester-user-456',
168
+ externalChatId: 'requester-chat-456',
169
+ status: 'active',
170
+ policy: 'allow',
171
+ });
172
+
173
+ // Simulate the ACL check that inbound-message-handler performs
174
+ const member = findMember({
175
+ assistantId: 'self',
176
+ sourceChannel: 'telegram',
177
+ externalUserId: 'requester-user-456',
178
+ externalChatId: 'requester-chat-456',
179
+ });
180
+
181
+ expect(member).not.toBeNull();
182
+ expect(member!.status).toBe('active');
183
+ expect(member!.policy).toBe('allow');
184
+ // ACL check passes: member exists, is active, and has allow policy
185
+ });
186
+
187
+ test('no cross-assistant leakage (member scoped correctly)', () => {
188
+ // Create member for assistant 'self'
189
+ const session = createOutboundSession({
190
+ assistantId: 'self',
191
+ channel: 'telegram',
192
+ expectedExternalUserId: 'user-cross-test',
193
+ expectedChatId: 'chat-cross-test',
194
+ identityBindingStatus: 'bound',
195
+ destinationAddress: 'chat-cross-test',
196
+ verificationPurpose: 'trusted_contact',
197
+ });
198
+
199
+ validateAndConsumeChallenge(
200
+ 'self', 'telegram', session.secret,
201
+ 'user-cross-test', 'chat-cross-test',
202
+ );
203
+
204
+ upsertMember({
205
+ assistantId: 'self',
206
+ sourceChannel: 'telegram',
207
+ externalUserId: 'user-cross-test',
208
+ externalChatId: 'chat-cross-test',
209
+ status: 'active',
210
+ policy: 'allow',
211
+ });
212
+
213
+ // Member should be found for 'self'
214
+ const selfMember = findMember({
215
+ assistantId: 'self',
216
+ sourceChannel: 'telegram',
217
+ externalUserId: 'user-cross-test',
218
+ });
219
+ expect(selfMember).not.toBeNull();
220
+ expect(selfMember!.status).toBe('active');
221
+
222
+ // Member should NOT be found for a different assistant
223
+ const otherMember = findMember({
224
+ assistantId: 'other-assistant',
225
+ sourceChannel: 'telegram',
226
+ externalUserId: 'user-cross-test',
227
+ });
228
+ expect(otherMember).toBeNull();
229
+ });
230
+
231
+ test('re-verification of previously revoked member reactivates them', () => {
232
+ // Create and activate a member
233
+ const member = upsertMember({
234
+ assistantId: 'self',
235
+ sourceChannel: 'telegram',
236
+ externalUserId: 'user-revoked',
237
+ externalChatId: 'chat-revoked',
238
+ status: 'active',
239
+ policy: 'allow',
240
+ displayName: 'Revoked User',
241
+ });
242
+
243
+ // Revoke the member
244
+ const revoked = revokeMember(member.id, 'testing revocation');
245
+ expect(revoked).not.toBeNull();
246
+ expect(revoked!.status).toBe('revoked');
247
+
248
+ // Verify the member is indeed revoked (ACL would reject)
249
+ const revokedMember = findMember({
250
+ assistantId: 'self',
251
+ sourceChannel: 'telegram',
252
+ externalUserId: 'user-revoked',
253
+ });
254
+ expect(revokedMember).not.toBeNull();
255
+ expect(revokedMember!.status).toBe('revoked');
256
+
257
+ // Guardian re-approves, new outbound session created
258
+ const session = createOutboundSession({
259
+ assistantId: 'self',
260
+ channel: 'telegram',
261
+ expectedExternalUserId: 'user-revoked',
262
+ expectedChatId: 'chat-revoked',
263
+ identityBindingStatus: 'bound',
264
+ destinationAddress: 'chat-revoked',
265
+ verificationPurpose: 'trusted_contact',
266
+ });
267
+
268
+ // Requester enters the new code
269
+ const result = validateAndConsumeChallenge(
270
+ 'self', 'telegram', session.secret,
271
+ 'user-revoked', 'chat-revoked',
272
+ );
273
+ expect(result.success).toBe(true);
274
+ if (result.success) {
275
+ expect(result.verificationType).toBe('trusted_contact');
276
+ }
277
+
278
+ // upsertMember reactivates the existing record
279
+ upsertMember({
280
+ assistantId: 'self',
281
+ sourceChannel: 'telegram',
282
+ externalUserId: 'user-revoked',
283
+ externalChatId: 'chat-revoked',
284
+ status: 'active',
285
+ policy: 'allow',
286
+ });
287
+
288
+ // Verify: member is now active again
289
+ const reactivated = findMember({
290
+ assistantId: 'self',
291
+ sourceChannel: 'telegram',
292
+ externalUserId: 'user-revoked',
293
+ });
294
+ expect(reactivated).not.toBeNull();
295
+ expect(reactivated!.status).toBe('active');
296
+ expect(reactivated!.policy).toBe('allow');
297
+ });
298
+
299
+ test('trusted contact verification does NOT create a guardian binding', () => {
300
+ // Ensure there's an existing guardian binding we want to preserve
301
+ createBinding({
302
+ assistantId: 'self',
303
+ channel: 'telegram',
304
+ guardianExternalUserId: 'guardian-user-original',
305
+ guardianDeliveryChatId: 'guardian-chat-original',
306
+ verifiedVia: 'challenge',
307
+ metadataJson: null,
308
+ });
309
+
310
+ // Create an outbound session for a requester (different user than guardian)
311
+ const session = createOutboundSession({
312
+ assistantId: 'self',
313
+ channel: 'telegram',
314
+ expectedExternalUserId: 'requester-user-789',
315
+ expectedChatId: 'requester-chat-789',
316
+ identityBindingStatus: 'bound',
317
+ destinationAddress: 'requester-chat-789',
318
+ verificationPurpose: 'trusted_contact',
319
+ });
320
+
321
+ const result = validateAndConsumeChallenge(
322
+ 'self', 'telegram', session.secret,
323
+ 'requester-user-789', 'requester-chat-789',
324
+ );
325
+
326
+ expect(result.success).toBe(true);
327
+ if (result.success) {
328
+ expect(result.verificationType).toBe('trusted_contact');
329
+ // Should NOT have a bindingId — no guardian binding created
330
+ expect('bindingId' in result).toBe(false);
331
+ }
332
+
333
+ // The original guardian binding should remain intact
334
+ const binding = getActiveBinding('self', 'telegram');
335
+ expect(binding).not.toBeNull();
336
+ expect(binding!.guardianExternalUserId).toBe('guardian-user-original');
337
+ });
338
+
339
+ test('guardian inbound verification still creates binding (backward compat)', () => {
340
+ // Create an inbound challenge (no expected identity — guardian flow)
341
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
342
+ const { createVerificationChallenge } = require('../runtime/channel-guardian-service.js');
343
+ const { secret } = createVerificationChallenge('self', 'telegram');
344
+
345
+ const result = validateAndConsumeChallenge(
346
+ 'self', 'telegram', secret,
347
+ 'guardian-user', 'guardian-chat',
348
+ );
349
+
350
+ expect(result.success).toBe(true);
351
+ if (result.success && result.verificationType === 'guardian') {
352
+ expect(result.bindingId).toBeDefined();
353
+ }
354
+
355
+ // Guardian binding should be created
356
+ const binding = getActiveBinding('self', 'telegram');
357
+ expect(binding).not.toBeNull();
358
+ expect(binding!.guardianExternalUserId).toBe('guardian-user');
359
+ });
360
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ appendReleaseBlock,
5
+ extractReleaseIds,
6
+ hasReleaseBlock,
7
+ releaseMarker,
8
+ } from '../config/update-bulletin-format.js';
9
+
10
+ describe('releaseMarker', () => {
11
+ test('returns an HTML comment with the version embedded', () => {
12
+ expect(releaseMarker('1.2.3')).toBe('<!-- vellum-update-release:1.2.3 -->');
13
+ });
14
+
15
+ test('handles pre-release / build-metadata versions', () => {
16
+ expect(releaseMarker('2.0.0-beta.1+build.42')).toBe(
17
+ '<!-- vellum-update-release:2.0.0-beta.1+build.42 -->',
18
+ );
19
+ });
20
+ });
21
+
22
+ describe('hasReleaseBlock', () => {
23
+ const content = [
24
+ '<!-- vellum-update-release:1.0.0 -->',
25
+ '## 1.0.0',
26
+ 'Initial release.',
27
+ '',
28
+ '<!-- vellum-update-release:1.1.0 -->',
29
+ '## 1.1.0',
30
+ 'Second release.',
31
+ ].join('\n');
32
+
33
+ test('returns true when the marker is present', () => {
34
+ expect(hasReleaseBlock(content, '1.0.0')).toBe(true);
35
+ expect(hasReleaseBlock(content, '1.1.0')).toBe(true);
36
+ });
37
+
38
+ test('returns false when the marker is absent', () => {
39
+ expect(hasReleaseBlock(content, '2.0.0')).toBe(false);
40
+ });
41
+
42
+ test('returns false for empty content', () => {
43
+ expect(hasReleaseBlock('', '1.0.0')).toBe(false);
44
+ });
45
+ });
46
+
47
+ describe('appendReleaseBlock', () => {
48
+ test('appends to empty content', () => {
49
+ const result = appendReleaseBlock('', '1.0.0', '## 1.0.0\nInitial release.');
50
+ expect(result).toBe(
51
+ '<!-- vellum-update-release:1.0.0 -->\n## 1.0.0\nInitial release.\n',
52
+ );
53
+ });
54
+
55
+ test('preserves prior blocks when appending', () => {
56
+ const existing =
57
+ '<!-- vellum-update-release:1.0.0 -->\n## 1.0.0\nFirst.\n';
58
+ const result = appendReleaseBlock(existing, '1.1.0', '## 1.1.0\nSecond.');
59
+
60
+ // Prior block is untouched
61
+ expect(result).toContain('<!-- vellum-update-release:1.0.0 -->');
62
+ expect(result).toContain('## 1.0.0\nFirst.');
63
+
64
+ // New block is appended
65
+ expect(result).toContain('<!-- vellum-update-release:1.1.0 -->');
66
+ expect(result).toContain('## 1.1.0\nSecond.');
67
+
68
+ // New block comes after old block
69
+ const oldIdx = result.indexOf('<!-- vellum-update-release:1.0.0 -->');
70
+ const newIdx = result.indexOf('<!-- vellum-update-release:1.1.0 -->');
71
+ expect(newIdx).toBeGreaterThan(oldIdx);
72
+ });
73
+
74
+ test('inserts separator when existing content lacks trailing newline', () => {
75
+ const existing = '<!-- vellum-update-release:1.0.0 -->\nFirst.';
76
+ const result = appendReleaseBlock(existing, '1.1.0', 'Second.');
77
+
78
+ // Double newline separates the blocks when there was no trailing newline
79
+ expect(result).toContain('First.\n\n<!-- vellum-update-release:1.1.0 -->');
80
+ });
81
+ });
82
+
83
+ describe('extractReleaseIds', () => {
84
+ test('returns all version strings from multiple markers', () => {
85
+ const content = [
86
+ '<!-- vellum-update-release:1.0.0 -->',
87
+ 'Block one.',
88
+ '<!-- vellum-update-release:1.1.0 -->',
89
+ 'Block two.',
90
+ '<!-- vellum-update-release:2.0.0-rc.1 -->',
91
+ 'Block three.',
92
+ ].join('\n');
93
+
94
+ expect(extractReleaseIds(content)).toEqual([
95
+ '1.0.0',
96
+ '1.1.0',
97
+ '2.0.0-rc.1',
98
+ ]);
99
+ });
100
+
101
+ test('returns empty array for empty content', () => {
102
+ expect(extractReleaseIds('')).toEqual([]);
103
+ });
104
+
105
+ test('returns empty array when no markers are present', () => {
106
+ expect(extractReleaseIds('Just some text\nwith no markers.')).toEqual([]);
107
+ });
108
+
109
+ test('handles duplicate markers', () => {
110
+ const content = [
111
+ '<!-- vellum-update-release:1.0.0 -->',
112
+ 'Block one.',
113
+ '<!-- vellum-update-release:1.0.0 -->',
114
+ 'Duplicate block.',
115
+ ].join('\n');
116
+
117
+ expect(extractReleaseIds(content)).toEqual(['1.0.0', '1.0.0']);
118
+ });
119
+ });
@@ -0,0 +1,129 @@
1
+ import { beforeEach, describe, expect, it, mock } from 'bun:test';
2
+
3
+ const store = new Map<string, string>();
4
+
5
+ mock.module('../memory/checkpoints.js', () => ({
6
+ getMemoryCheckpoint: mock((key: string) => store.get(key) ?? null),
7
+ setMemoryCheckpoint: mock((key: string, value: string) => store.set(key, value)),
8
+ }));
9
+
10
+ const {
11
+ getActiveReleases,
12
+ setActiveReleases,
13
+ getCompletedReleases,
14
+ setCompletedReleases,
15
+ isReleaseCompleted,
16
+ markReleasesCompleted,
17
+ addActiveRelease,
18
+ } = await import('../config/update-bulletin-state.js');
19
+
20
+ describe('update-bulletin-state', () => {
21
+ beforeEach(() => {
22
+ store.clear();
23
+ });
24
+
25
+ describe('empty/default state', () => {
26
+ it('returns empty array when no active releases checkpoint exists', () => {
27
+ expect(getActiveReleases()).toEqual([]);
28
+ });
29
+
30
+ it('returns empty array when no completed releases checkpoint exists', () => {
31
+ expect(getCompletedReleases()).toEqual([]);
32
+ });
33
+
34
+ it('isReleaseCompleted returns false when no completed releases exist', () => {
35
+ expect(isReleaseCompleted('1.0.0')).toBe(false);
36
+ });
37
+ });
38
+
39
+ describe('corrupt checkpoint content', () => {
40
+ it('returns empty array for invalid JSON in active releases', () => {
41
+ store.set('updates:active_releases', 'not-json{{{');
42
+ expect(getActiveReleases()).toEqual([]);
43
+ });
44
+
45
+ it('returns empty array for invalid JSON in completed releases', () => {
46
+ store.set('updates:completed_releases', '}{broken');
47
+ expect(getCompletedReleases()).toEqual([]);
48
+ });
49
+
50
+ it('returns empty array when checkpoint contains a non-array JSON value', () => {
51
+ store.set('updates:active_releases', '"just-a-string"');
52
+ expect(getActiveReleases()).toEqual([]);
53
+ });
54
+
55
+ it('filters out non-string values from the array', () => {
56
+ store.set('updates:active_releases', '["1.0.0", 42, null, "2.0.0"]');
57
+ expect(getActiveReleases()).toEqual(['1.0.0', '2.0.0']);
58
+ });
59
+ });
60
+
61
+ describe('round-trip serialization', () => {
62
+ it('write then read returns same data for active releases', () => {
63
+ const releases = ['1.0.0', '2.0.0', '3.0.0'];
64
+ setActiveReleases(releases);
65
+ expect(getActiveReleases()).toEqual(releases);
66
+ });
67
+
68
+ it('write then read returns same data for completed releases', () => {
69
+ const releases = ['0.9.0', '1.0.0'];
70
+ setCompletedReleases(releases);
71
+ expect(getCompletedReleases()).toEqual(releases);
72
+ });
73
+
74
+ it('isReleaseCompleted returns true for a completed release', () => {
75
+ setCompletedReleases(['1.0.0', '2.0.0']);
76
+ expect(isReleaseCompleted('1.0.0')).toBe(true);
77
+ expect(isReleaseCompleted('2.0.0')).toBe(true);
78
+ expect(isReleaseCompleted('3.0.0')).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('dedupe behavior', () => {
83
+ it('setActiveReleases deduplicates entries', () => {
84
+ setActiveReleases(['1.0.0', '2.0.0', '1.0.0', '2.0.0', '1.0.0']);
85
+ expect(getActiveReleases()).toEqual(['1.0.0', '2.0.0']);
86
+ });
87
+
88
+ it('setCompletedReleases deduplicates entries', () => {
89
+ setCompletedReleases(['a', 'b', 'a']);
90
+ expect(getCompletedReleases()).toEqual(['a', 'b']);
91
+ });
92
+
93
+ it('addActiveRelease does not duplicate an existing release', () => {
94
+ setActiveReleases(['1.0.0']);
95
+ addActiveRelease('1.0.0');
96
+ expect(getActiveReleases()).toEqual(['1.0.0']);
97
+ });
98
+
99
+ it('markReleasesCompleted does not duplicate existing entries', () => {
100
+ setCompletedReleases(['1.0.0']);
101
+ markReleasesCompleted(['1.0.0', '2.0.0']);
102
+ expect(getCompletedReleases()).toEqual(['1.0.0', '2.0.0']);
103
+ });
104
+ });
105
+
106
+ describe('sort behavior', () => {
107
+ it('active releases are sorted alphabetically', () => {
108
+ setActiveReleases(['c-release', 'a-release', 'b-release']);
109
+ expect(getActiveReleases()).toEqual(['a-release', 'b-release', 'c-release']);
110
+ });
111
+
112
+ it('completed releases are sorted alphabetically', () => {
113
+ setCompletedReleases(['3.0.0', '1.0.0', '2.0.0']);
114
+ expect(getCompletedReleases()).toEqual(['1.0.0', '2.0.0', '3.0.0']);
115
+ });
116
+
117
+ it('addActiveRelease maintains sorted order', () => {
118
+ setActiveReleases(['a', 'c']);
119
+ addActiveRelease('b');
120
+ expect(getActiveReleases()).toEqual(['a', 'b', 'c']);
121
+ });
122
+
123
+ it('markReleasesCompleted maintains sorted order', () => {
124
+ setCompletedReleases(['c']);
125
+ markReleasesCompleted(['a', 'b']);
126
+ expect(getCompletedReleases()).toEqual(['a', 'b', 'c']);
127
+ });
128
+ });
129
+ });