@vellumai/assistant 0.4.32 → 0.4.33

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 (87) hide show
  1. package/docs/architecture/memory.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/access-request-decision.test.ts +83 -1
  4. package/src/__tests__/actor-token-service.test.ts +0 -1
  5. package/src/__tests__/approval-routes-http.test.ts +0 -1
  6. package/src/__tests__/call-controller.test.ts +0 -1
  7. package/src/__tests__/call-routes-http.test.ts +0 -1
  8. package/src/__tests__/channel-guardian.test.ts +0 -1
  9. package/src/__tests__/channel-invite-transport.test.ts +52 -40
  10. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
  11. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
  12. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  13. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  14. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  15. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  16. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  17. package/src/__tests__/handlers-telegram-config.test.ts +0 -1
  18. package/src/__tests__/inbound-invite-redemption.test.ts +1 -4
  19. package/src/__tests__/ingress-reconcile.test.ts +3 -36
  20. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  21. package/src/__tests__/migration-export-http.test.ts +0 -1
  22. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  23. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  24. package/src/__tests__/migration-validate-http.test.ts +0 -1
  25. package/src/__tests__/non-member-access-request.test.ts +0 -1
  26. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  27. package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
  28. package/src/__tests__/relay-server.test.ts +145 -2
  29. package/src/__tests__/sandbox-host-parity.test.ts +5 -2
  30. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  31. package/src/__tests__/slack-channel-config.test.ts +0 -1
  32. package/src/__tests__/slack-inbound-verification.test.ts +0 -1
  33. package/src/__tests__/sms-messaging-provider.test.ts +0 -4
  34. package/src/__tests__/terminal-tools.test.ts +5 -2
  35. package/src/__tests__/trusted-contact-approval-notifier.test.ts +66 -74
  36. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  37. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
  38. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  39. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  40. package/src/__tests__/twilio-routes.test.ts +0 -1
  41. package/src/__tests__/update-bulletin.test.ts +0 -2
  42. package/src/__tests__/user-reference.test.ts +47 -1
  43. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  44. package/src/__tests__/workspace-git-service.test.ts +2 -2
  45. package/src/calls/relay-server.ts +5 -55
  46. package/src/channels/config.ts +41 -2
  47. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  48. package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
  49. package/src/config/env.ts +0 -4
  50. package/src/config/feature-flag-registry.json +4 -4
  51. package/src/config/user-reference.ts +47 -9
  52. package/src/daemon/handlers/config-channels.ts +11 -10
  53. package/src/daemon/handlers/contacts.ts +5 -1
  54. package/src/daemon/lifecycle.ts +18 -26
  55. package/src/memory/channel-delivery-store.ts +1 -0
  56. package/src/memory/db-init.ts +4 -0
  57. package/src/memory/delivery-crud.ts +13 -0
  58. package/src/memory/invite-store.ts +71 -1
  59. package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
  60. package/src/memory/migrations/index.ts +1 -0
  61. package/src/memory/schema/contacts.ts +2 -0
  62. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
  63. package/src/runtime/auth/token-service.ts +50 -0
  64. package/src/runtime/channel-guardian-service.ts +1 -3
  65. package/src/runtime/channel-invite-transport.ts +121 -34
  66. package/src/runtime/channel-invite-transports/email.ts +50 -0
  67. package/src/runtime/channel-invite-transports/slack.ts +81 -0
  68. package/src/runtime/channel-invite-transports/sms.ts +70 -0
  69. package/src/runtime/channel-invite-transports/telegram.ts +29 -11
  70. package/src/runtime/channel-invite-transports/voice.ts +12 -12
  71. package/src/runtime/invite-redemption-service.ts +193 -0
  72. package/src/runtime/invite-redemption-templates.ts +6 -6
  73. package/src/runtime/invite-service.ts +81 -11
  74. package/src/runtime/routes/access-request-decision.ts +52 -6
  75. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -0
  76. package/src/runtime/routes/contact-routes.ts +33 -6
  77. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -6
  78. package/src/runtime/routes/inbound-message-handler.ts +1 -3
  79. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +289 -4
  80. package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -42
  81. package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
  82. package/src/runtime/routes/invite-routes.ts +1 -0
  83. package/src/tools/browser/browser-manager.ts +10 -1
  84. package/src/tools/browser/runtime-check.ts +3 -1
  85. package/src/tools/shared/shell-output.ts +7 -2
  86. package/src/util/platform.ts +0 -4
  87. package/src/workspace/git-service.ts +10 -4
@@ -487,7 +487,7 @@ graph TB
487
487
 
488
488
  ### How it works
489
489
 
490
- 1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token`, `http-token`), sets the git identity to "Vellum Assistant", and creates an initial baseline commit capturing any pre-existing files. The baseline commit is intentional — it makes `git log`, `git diff`, and `git revert` work cleanly from the start. Both new and existing workspaces get the same treatment. For existing repos (e.g. created by older versions or external tools), `.gitignore` rules and git identity are set idempotently on each init, ensuring proper configuration regardless of how the repo was originally created.
490
+ 1. **Lazy initialization**: The git repository is created on first use, not at workspace creation. When `ensureInitialized()` is called, it checks for a `.git` directory. If absent, it runs `git init`, creates a `.gitignore` (excluding `data/`, `logs/`, `*.log`, `*.sock`, `*.pid`, `session-token`), sets the git identity to "Vellum Assistant", and creates an initial baseline commit capturing any pre-existing files. The baseline commit is intentional — it makes `git log`, `git diff`, and `git revert` work cleanly from the start. Both new and existing workspaces get the same treatment. For existing repos (e.g. created by older versions or external tools), `.gitignore` rules and git identity are set idempotently on each init, ensuring proper configuration regardless of how the repo was originally created.
491
491
 
492
492
  2. **Turn-boundary commits**: After each conversation turn (user message + assistant response cycle), `session.ts` commits workspace changes via `commitTurnChanges(workspaceDir, sessionId, turnNumber)`. The commit runs in the `finally` block of `runAgentLoop`, guarded by a `turnStarted` flag that is set once the agent loop begins executing. This guarantees a commit attempt even when post-processing (e.g. `resolveAssistantAttachments`) throws, or when the user cancels mid-turn. The commit is raced against a configurable timeout (`workspaceGit.turnCommitMaxWaitMs`, default 4s) via `Promise.race`. If the commit exceeds the timeout, the turn proceeds immediately while the commit continues in the background. Note: the background commit is NOT awaited before the next turn starts, so brief cross-turn file attribution windows are possible but accepted as a tradeoff for responsiveness. Commit outcomes are logged with structured fields (`sessionId`, `turnNumber`, `filesChanged`, `durationMs`) for observability.
493
493
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.32",
3
+ "version": "0.4.33",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -30,7 +30,6 @@ mock.module("../util/platform.js", () => ({
30
30
  getDbPath: () => join(testDir, "test.db"),
31
31
  getLogPath: () => join(testDir, "test.log"),
32
32
  ensureDataDir: () => {},
33
- readHttpToken: () => "test-bearer-token",
34
33
  }));
35
34
 
36
35
  mock.module("../util/logger.js", () => ({
@@ -331,4 +330,87 @@ describe("access request notification delivery", () => {
331
330
  expect(text).toContain("unable to deliver");
332
331
  expect(text).toContain("try again");
333
332
  });
333
+
334
+ test("slack approval notification is sent as DM using requesterExternalUserId", async () => {
335
+ await notifyRequesterOfApproval({
336
+ replyCallbackUrl:
337
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
338
+ requesterChatId: "C12345-channel",
339
+ requesterExternalUserId: "U98765-user",
340
+ channel: "slack",
341
+ assistantId: "self",
342
+ bearerToken: "test-token",
343
+ });
344
+
345
+ expect(deliverReplyCalls.length).toBe(1);
346
+ const call = deliverReplyCalls[0];
347
+ // Should target the user ID (DM) not the channel
348
+ expect(call.payload.chatId).toBe("U98765-user");
349
+ // threadTs should be stripped — it belongs to the guardian's channel thread
350
+ expect(call.url).not.toContain("threadTs");
351
+ });
352
+
353
+ test("slack denial notification is sent as DM using requesterExternalUserId", async () => {
354
+ await notifyRequesterOfDenial({
355
+ replyCallbackUrl:
356
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
357
+ requesterChatId: "C12345-channel",
358
+ requesterExternalUserId: "U98765-user",
359
+ channel: "slack",
360
+ assistantId: "self",
361
+ bearerToken: "test-token",
362
+ });
363
+
364
+ expect(deliverReplyCalls.length).toBe(1);
365
+ const call = deliverReplyCalls[0];
366
+ expect(call.payload.chatId).toBe("U98765-user");
367
+ expect(call.url).not.toContain("threadTs");
368
+ });
369
+
370
+ test("slack delivery failure notification is sent as DM using requesterExternalUserId", async () => {
371
+ await notifyRequesterOfDeliveryFailure({
372
+ replyCallbackUrl:
373
+ "http://localhost:7830/deliver/slack?threadTs=1234.5678",
374
+ requesterChatId: "C12345-channel",
375
+ requesterExternalUserId: "U98765-user",
376
+ channel: "slack",
377
+ assistantId: "self",
378
+ bearerToken: "test-token",
379
+ });
380
+
381
+ expect(deliverReplyCalls.length).toBe(1);
382
+ const call = deliverReplyCalls[0];
383
+ expect(call.payload.chatId).toBe("U98765-user");
384
+ expect(call.url).not.toContain("threadTs");
385
+ });
386
+
387
+ test("non-slack channels still use requesterChatId and preserve threadTs", async () => {
388
+ await notifyRequesterOfApproval({
389
+ replyCallbackUrl:
390
+ "http://localhost:7830/deliver/telegram?threadTs=1234.5678",
391
+ requesterChatId: "chat-123",
392
+ requesterExternalUserId: "user-456",
393
+ channel: "telegram",
394
+ assistantId: "self",
395
+ bearerToken: "test-token",
396
+ });
397
+
398
+ expect(deliverReplyCalls.length).toBe(1);
399
+ expect(deliverReplyCalls[0].payload.chatId).toBe("chat-123");
400
+ // threadTs should be preserved for non-slack channels
401
+ expect(deliverReplyCalls[0].url).toContain("threadTs=1234.5678");
402
+ });
403
+
404
+ test("slack without requesterExternalUserId falls back to requesterChatId", async () => {
405
+ await notifyRequesterOfApproval({
406
+ replyCallbackUrl: "http://localhost:7830/deliver/slack",
407
+ requesterChatId: "C12345-channel",
408
+ channel: "slack",
409
+ assistantId: "self",
410
+ bearerToken: "test-token",
411
+ });
412
+
413
+ expect(deliverReplyCalls.length).toBe(1);
414
+ expect(deliverReplyCalls[0].payload.chatId).toBe("C12345-channel");
415
+ });
334
416
  });
@@ -39,7 +39,6 @@ mock.module("../config/env.js", () => ({
39
39
  isHttpAuthDisabled: () => false,
40
40
  getInternalGatewayTarget: () => "http://localhost:7822",
41
41
  getGatewayBaseUrl: () => "http://localhost:7822",
42
- getRuntimeProxyBearerToken: () => undefined,
43
42
  getRuntimeGatewayOriginSecret: () => undefined,
44
43
  isHttpAuthDisabledWithoutSafetyGate: () => false,
45
44
  getEnableMonitoring: () => false,
@@ -57,7 +57,6 @@ mock.module("../config/env.js", () => ({
57
57
  getGatewayPort: () => 7830,
58
58
  getRuntimeHttpPort: () => 7821,
59
59
  getRuntimeHttpHost: () => "127.0.0.1",
60
- getRuntimeProxyBearerToken: () => undefined,
61
60
  getRuntimeGatewayOriginSecret: () => undefined,
62
61
  getIngressPublicBaseUrl: () => undefined,
63
62
  setIngressPublicBaseUrl: () => {},
@@ -27,7 +27,6 @@ mock.module("../util/platform.js", () => ({
27
27
  getDbPath: () => join(testDir, "test.db"),
28
28
  getLogPath: () => join(testDir, "test.log"),
29
29
  ensureDataDir: () => {},
30
- readHttpToken: () => null,
31
30
  }));
32
31
 
33
32
  mock.module("../util/logger.js", () => ({
@@ -27,7 +27,6 @@ mock.module("../util/platform.js", () => ({
27
27
  getDbPath: () => join(testDir, "test.db"),
28
28
  getLogPath: () => join(testDir, "test.log"),
29
29
  ensureDataDir: () => {},
30
- readHttpToken: () => null,
31
30
  }));
32
31
 
33
32
  mock.module("../util/logger.js", () => ({
@@ -21,7 +21,6 @@ mock.module("../util/platform.js", () => ({
21
21
  getDbPath: () => join(testDir, "test.db"),
22
22
  getLogPath: () => join(testDir, "test.log"),
23
23
  ensureDataDir: () => {},
24
- readHttpToken: () => "test-bearer-token",
25
24
  }));
26
25
 
27
26
  mock.module("../util/logger.js", () => ({
@@ -17,19 +17,19 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
17
17
 
18
18
  import {
19
19
  _resetRegistry,
20
- type ChannelInviteTransport,
20
+ type ChannelInviteAdapter,
21
+ getInviteAdapterRegistry,
21
22
  getTransport,
22
23
  registerTransport,
23
24
  } from "../runtime/channel-invite-transport.js";
24
- // Importing the Telegram module auto-registers the transport
25
- import { telegramInviteTransport } from "../runtime/channel-invite-transports/telegram.js";
25
+ import { telegramInviteAdapter } from "../runtime/channel-invite-transports/telegram.js";
26
26
 
27
27
  describe("channel-invite-transport", () => {
28
28
  beforeEach(() => {
29
29
  _resetRegistry();
30
30
  mockBotUsername = "test_invite_bot";
31
31
  // Re-register after reset so Telegram tests work
32
- registerTransport(telegramInviteTransport);
32
+ registerTransport(telegramInviteAdapter);
33
33
  });
34
34
 
35
35
  // =========================================================================
@@ -37,46 +37,58 @@ describe("channel-invite-transport", () => {
37
37
  // =========================================================================
38
38
 
39
39
  describe("registry", () => {
40
- test("returns the Telegram transport for telegram channel", () => {
41
- const transport = getTransport("telegram");
42
- expect(transport).toBeDefined();
43
- expect(transport!.channel).toBe("telegram");
40
+ test("returns the Telegram adapter for telegram channel", () => {
41
+ const adapter = getTransport("telegram");
42
+ expect(adapter).toBeDefined();
43
+ expect(adapter!.channel).toBe("telegram");
44
44
  });
45
45
 
46
46
  test("returns undefined for an unregistered channel", () => {
47
- const transport = getTransport("sms");
48
- expect(transport).toBeUndefined();
47
+ const adapter = getTransport("sms");
48
+ expect(adapter).toBeUndefined();
49
49
  });
50
50
 
51
- test("overwrites a previously registered transport for the same channel", () => {
52
- const custom: ChannelInviteTransport = {
51
+ test("overwrites a previously registered adapter for the same channel", () => {
52
+ const custom: ChannelInviteAdapter = {
53
53
  channel: "telegram",
54
- buildShareableInvite: () => ({ url: "custom", displayText: "custom" }),
54
+ buildShareLink: () => ({ url: "custom", displayText: "custom" }),
55
55
  extractInboundToken: () => undefined,
56
56
  };
57
57
  registerTransport(custom);
58
- const transport = getTransport("telegram");
58
+ const adapter = getTransport("telegram");
59
59
  expect(
60
- transport!.buildShareableInvite({
60
+ adapter!.buildShareLink!({
61
61
  rawToken: "x",
62
62
  sourceChannel: "telegram",
63
63
  }).url,
64
64
  ).toBe("custom");
65
65
  });
66
66
 
67
- test("_resetRegistry clears all transports", () => {
67
+ test("_resetRegistry clears all adapters", () => {
68
68
  _resetRegistry();
69
69
  expect(getTransport("telegram")).toBeUndefined();
70
70
  });
71
+
72
+ test("getInviteAdapterRegistry returns the singleton registry", () => {
73
+ const registry = getInviteAdapterRegistry();
74
+ expect(registry.get("telegram")).toBeDefined();
75
+ });
76
+
77
+ test("registry.getAll returns all registered adapters", () => {
78
+ const registry = getInviteAdapterRegistry();
79
+ const all = registry.getAll();
80
+ expect(all.length).toBeGreaterThanOrEqual(1);
81
+ expect(all.some((a) => a.channel === "telegram")).toBe(true);
82
+ });
71
83
  });
72
84
 
73
85
  // =========================================================================
74
- // Telegram adapter — buildShareableInvite
86
+ // Telegram adapter — buildShareLink
75
87
  // =========================================================================
76
88
 
77
- describe("telegram buildShareableInvite", () => {
89
+ describe("telegram buildShareLink", () => {
78
90
  test("produces a valid Telegram deep link", () => {
79
- const result = telegramInviteTransport.buildShareableInvite!({
91
+ const result = telegramInviteAdapter.buildShareLink!({
80
92
  rawToken: "abc123_test-token",
81
93
  sourceChannel: "telegram",
82
94
  });
@@ -90,11 +102,11 @@ describe("channel-invite-transport", () => {
90
102
  });
91
103
 
92
104
  test("deep link is deterministic for the same token", () => {
93
- const a = telegramInviteTransport.buildShareableInvite!({
105
+ const a = telegramInviteAdapter.buildShareLink!({
94
106
  rawToken: "tok1",
95
107
  sourceChannel: "telegram",
96
108
  });
97
- const b = telegramInviteTransport.buildShareableInvite!({
109
+ const b = telegramInviteAdapter.buildShareLink!({
98
110
  rawToken: "tok1",
99
111
  sourceChannel: "telegram",
100
112
  });
@@ -104,7 +116,7 @@ describe("channel-invite-transport", () => {
104
116
 
105
117
  test("uses the configured bot username", () => {
106
118
  mockBotUsername = "my_custom_bot";
107
- const result = telegramInviteTransport.buildShareableInvite!({
119
+ const result = telegramInviteAdapter.buildShareLink!({
108
120
  rawToken: "token",
109
121
  sourceChannel: "telegram",
110
122
  });
@@ -118,7 +130,7 @@ describe("channel-invite-transport", () => {
118
130
  delete process.env.TELEGRAM_BOT_USERNAME;
119
131
  try {
120
132
  expect(() =>
121
- telegramInviteTransport.buildShareableInvite!({
133
+ telegramInviteAdapter.buildShareLink!({
122
134
  rawToken: "token",
123
135
  sourceChannel: "telegram",
124
136
  }),
@@ -133,7 +145,7 @@ describe("channel-invite-transport", () => {
133
145
  const prev = process.env.TELEGRAM_BOT_USERNAME;
134
146
  process.env.TELEGRAM_BOT_USERNAME = "env_bot";
135
147
  try {
136
- const result = telegramInviteTransport.buildShareableInvite!({
148
+ const result = telegramInviteAdapter.buildShareLink!({
137
149
  rawToken: "token",
138
150
  sourceChannel: "telegram",
139
151
  });
@@ -154,7 +166,7 @@ describe("channel-invite-transport", () => {
154
166
 
155
167
  describe("telegram extractInboundToken", () => {
156
168
  test("extracts token from structured commandIntent", () => {
157
- const token = telegramInviteTransport.extractInboundToken({
169
+ const token = telegramInviteAdapter.extractInboundToken!({
158
170
  commandIntent: { type: "start", payload: "iv_abc123" },
159
171
  content: "/start iv_abc123",
160
172
  });
@@ -162,7 +174,7 @@ describe("channel-invite-transport", () => {
162
174
  });
163
175
 
164
176
  test("extracts base64url token from commandIntent", () => {
165
- const token = telegramInviteTransport.extractInboundToken({
177
+ const token = telegramInviteAdapter.extractInboundToken!({
166
178
  commandIntent: { type: "start", payload: "iv_YWJjMTIz-_test" },
167
179
  content: "/start iv_YWJjMTIz-_test",
168
180
  });
@@ -170,7 +182,7 @@ describe("channel-invite-transport", () => {
170
182
  });
171
183
 
172
184
  test("returns undefined when commandIntent has no payload", () => {
173
- const token = telegramInviteTransport.extractInboundToken({
185
+ const token = telegramInviteAdapter.extractInboundToken!({
174
186
  commandIntent: { type: "start" },
175
187
  content: "/start",
176
188
  });
@@ -178,7 +190,7 @@ describe("channel-invite-transport", () => {
178
190
  });
179
191
 
180
192
  test("returns undefined when commandIntent payload has wrong prefix (gv_)", () => {
181
- const token = telegramInviteTransport.extractInboundToken({
193
+ const token = telegramInviteAdapter.extractInboundToken!({
182
194
  commandIntent: { type: "start", payload: "gv_abc123" },
183
195
  content: "/start gv_abc123",
184
196
  });
@@ -186,7 +198,7 @@ describe("channel-invite-transport", () => {
186
198
  });
187
199
 
188
200
  test("returns undefined when commandIntent payload has no prefix", () => {
189
- const token = telegramInviteTransport.extractInboundToken({
201
+ const token = telegramInviteAdapter.extractInboundToken!({
190
202
  commandIntent: { type: "start", payload: "abc123" },
191
203
  content: "/start abc123",
192
204
  });
@@ -194,7 +206,7 @@ describe("channel-invite-transport", () => {
194
206
  });
195
207
 
196
208
  test("returns undefined when commandIntent type is not start", () => {
197
- const token = telegramInviteTransport.extractInboundToken({
209
+ const token = telegramInviteAdapter.extractInboundToken!({
198
210
  commandIntent: { type: "help", payload: "iv_abc123" },
199
211
  content: "/help iv_abc123",
200
212
  });
@@ -202,7 +214,7 @@ describe("channel-invite-transport", () => {
202
214
  });
203
215
 
204
216
  test("returns undefined when commandIntent payload is iv_ with empty token", () => {
205
- const token = telegramInviteTransport.extractInboundToken({
217
+ const token = telegramInviteAdapter.extractInboundToken!({
206
218
  commandIntent: { type: "start", payload: "iv_" },
207
219
  content: "/start iv_",
208
220
  });
@@ -210,7 +222,7 @@ describe("channel-invite-transport", () => {
210
222
  });
211
223
 
212
224
  test("returns undefined when commandIntent payload is iv_ with whitespace-only token", () => {
213
- const token = telegramInviteTransport.extractInboundToken({
225
+ const token = telegramInviteAdapter.extractInboundToken!({
214
226
  commandIntent: { type: "start", payload: "iv_ " },
215
227
  content: "/start iv_ ",
216
228
  });
@@ -218,49 +230,49 @@ describe("channel-invite-transport", () => {
218
230
  });
219
231
 
220
232
  test("extracts token from raw content fallback", () => {
221
- const token = telegramInviteTransport.extractInboundToken({
233
+ const token = telegramInviteAdapter.extractInboundToken!({
222
234
  content: "/start iv_abc123",
223
235
  });
224
236
  expect(token).toBe("abc123");
225
237
  });
226
238
 
227
239
  test("extracts token from raw content with extra whitespace", () => {
228
- const token = telegramInviteTransport.extractInboundToken({
240
+ const token = telegramInviteAdapter.extractInboundToken!({
229
241
  content: "/start iv_token123",
230
242
  });
231
243
  expect(token).toBe("token123");
232
244
  });
233
245
 
234
246
  test("returns undefined for empty content", () => {
235
- const token = telegramInviteTransport.extractInboundToken({
247
+ const token = telegramInviteAdapter.extractInboundToken!({
236
248
  content: "",
237
249
  });
238
250
  expect(token).toBeUndefined();
239
251
  });
240
252
 
241
253
  test("returns undefined for content without /start", () => {
242
- const token = telegramInviteTransport.extractInboundToken({
254
+ const token = telegramInviteAdapter.extractInboundToken!({
243
255
  content: "hello world",
244
256
  });
245
257
  expect(token).toBeUndefined();
246
258
  });
247
259
 
248
260
  test("returns undefined for /start without iv_ prefix in content", () => {
249
- const token = telegramInviteTransport.extractInboundToken({
261
+ const token = telegramInviteAdapter.extractInboundToken!({
250
262
  content: "/start gv_abc123",
251
263
  });
252
264
  expect(token).toBeUndefined();
253
265
  });
254
266
 
255
267
  test("returns undefined for malformed /start with only iv_ in content", () => {
256
- const token = telegramInviteTransport.extractInboundToken({
268
+ const token = telegramInviteAdapter.extractInboundToken!({
257
269
  content: "/start iv_",
258
270
  });
259
271
  expect(token).toBeUndefined();
260
272
  });
261
273
 
262
274
  test("prefers commandIntent over raw content", () => {
263
- const token = telegramInviteTransport.extractInboundToken({
275
+ const token = telegramInviteAdapter.extractInboundToken!({
264
276
  commandIntent: { type: "start", payload: "iv_from_intent" },
265
277
  content: "/start iv_from_content",
266
278
  });
@@ -269,7 +281,7 @@ describe("channel-invite-transport", () => {
269
281
 
270
282
  test("returns undefined when commandIntent rejects, even if content has token", () => {
271
283
  // commandIntent present but payload has wrong prefix
272
- const token = telegramInviteTransport.extractInboundToken({
284
+ const token = telegramInviteAdapter.extractInboundToken!({
273
285
  commandIntent: { type: "start", payload: "gv_abc123" },
274
286
  content: "/start iv_valid_token",
275
287
  });
@@ -2,15 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import {
6
- afterAll,
7
- afterEach,
8
- beforeAll,
9
- beforeEach,
10
- describe,
11
- expect,
12
- test,
13
- } from "bun:test";
5
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
6
 
15
7
  import {
16
8
  _resetEnrichmentService,
@@ -27,7 +19,9 @@ describe("CommitEnrichmentService", () => {
27
19
  let testDir: string;
28
20
  let gitService: WorkspaceGitService;
29
21
 
30
- beforeAll(async () => {
22
+ beforeEach(async () => {
23
+ _resetGitServiceRegistry();
24
+ _resetEnrichmentService();
31
25
  testDir = join(
32
26
  tmpdir(),
33
27
  `vellum-enrichment-test-${Date.now()}-${Math.random()
@@ -39,11 +33,6 @@ describe("CommitEnrichmentService", () => {
39
33
  await gitService.ensureInitialized();
40
34
  });
41
35
 
42
- beforeEach(() => {
43
- _resetGitServiceRegistry();
44
- _resetEnrichmentService();
45
- });
46
-
47
36
  afterEach(async () => {
48
37
  try {
49
38
  await getEnrichmentService().shutdown();
@@ -52,22 +41,6 @@ describe("CommitEnrichmentService", () => {
52
41
  }
53
42
  _resetEnrichmentService();
54
43
 
55
- // Remove stale index.lock left by async enrichment jobs that ran git
56
- // commands concurrently. Without this, the next test's createCommit()
57
- // can fail with "Unable to create index.lock: File exists".
58
- const lockFile = join(testDir, ".git", "index.lock");
59
- if (existsSync(lockFile)) {
60
- rmSync(lockFile, { force: true });
61
- }
62
- });
63
-
64
- afterAll(async () => {
65
- try {
66
- await getEnrichmentService().shutdown();
67
- } catch {
68
- /* ignore */
69
- }
70
- _resetEnrichmentService();
71
44
  if (existsSync(testDir)) {
72
45
  rmSync(testDir, { recursive: true, force: true });
73
46
  }
@@ -86,13 +59,6 @@ describe("CommitEnrichmentService", () => {
86
59
  }
87
60
 
88
61
  async function createCommit(): Promise<string> {
89
- // Remove stale index.lock left by async enrichment jobs that ran git
90
- // commands concurrently in a previous test. Without this, git add -A
91
- // can fail with "Unable to create index.lock: File exists".
92
- const lockFile = join(testDir, ".git", "index.lock");
93
- if (existsSync(lockFile)) {
94
- rmSync(lockFile, { force: true });
95
- }
96
62
  writeFileSync(join(testDir, `file-${Date.now()}.txt`), "content");
97
63
  await gitService.commitChanges("test commit");
98
64
  return await gitService.getHeadHash();
@@ -49,7 +49,6 @@ mock.module("../util/platform.js", () => ({
49
49
  isMacOS: () => false,
50
50
  isLinux: () => true,
51
51
  isWindows: () => false,
52
- readHttpToken: () => null,
53
52
  normalizeAssistantId: (id: string) => id,
54
53
  }));
55
54
 
@@ -229,6 +229,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
229
229
  "calls/twilio-config.ts", // call infrastructure credential lookup
230
230
  "calls/twilio-provider.ts", // call infrastructure credential lookup
231
231
  "calls/twilio-rest.ts", // Twilio REST API credential lookup
232
+ "runtime/channel-invite-transports/sms.ts", // SMS invite transport phone number lookup
232
233
  "cli/config-commands.ts", // CLI credential management commands
233
234
  "runtime/http-server.ts", // HTTP server credential lookup
234
235
  "daemon/handlers/twitter-auth.ts", // Twitter OAuth token storage
@@ -31,7 +31,6 @@ mock.module("../util/platform.js", () => ({
31
31
  getDbPath: () => join(testDir, "test.db"),
32
32
  getLogPath: () => join(testDir, "test.log"),
33
33
  ensureDataDir: () => {},
34
- readHttpToken: () => "test-bearer-token",
35
34
  }));
36
35
 
37
36
  mock.module("../util/logger.js", () => ({
@@ -17,7 +17,6 @@ mock.module("../util/platform.js", () => ({
17
17
  getDbPath: () => join(testDir, "test.db"),
18
18
  getLogPath: () => join(testDir, "test.log"),
19
19
  ensureDataDir: () => {},
20
- readHttpToken: () => "test-token",
21
20
  }));
22
21
 
23
22
  mock.module("../util/logger.js", () => ({
@@ -18,7 +18,6 @@ mock.module("../util/platform.js", () => ({
18
18
  getDbPath: () => join(testDir, "test.db"),
19
19
  getLogPath: () => join(testDir, "test.log"),
20
20
  ensureDataDir: () => {},
21
- readHttpToken: () => null,
22
21
  }));
23
22
 
24
23
  mock.module("../util/logger.js", () => ({
@@ -34,7 +34,6 @@ mock.module("../util/platform.js", () => ({
34
34
  getDbPath: () => join(testDir, "test.db"),
35
35
  getLogPath: () => join(testDir, "test.log"),
36
36
  ensureDataDir: () => {},
37
- readHttpToken: () => "test-bearer-token",
38
37
  }));
39
38
 
40
39
  mock.module("../util/logger.js", () => ({
@@ -34,7 +34,6 @@ mock.module("../util/platform.js", () => ({
34
34
  getDbPath: () => join(testDir, "test.db"),
35
35
  getLogPath: () => join(testDir, "test.log"),
36
36
  ensureDataDir: () => {},
37
- readHttpToken: () => undefined,
38
37
  }));
39
38
 
40
39
  mock.module("../util/logger.js", () => ({
@@ -27,7 +27,6 @@ mock.module("../util/platform.js", () => ({
27
27
  getDbPath: () => join(testDir, "test.db"),
28
28
  getLogPath: () => join(testDir, "test.log"),
29
29
  ensureDataDir: () => {},
30
- readHttpToken: () => "test-bearer-token",
31
30
  }));
32
31
 
33
32
  mock.module("../util/logger.js", () => ({
@@ -204,9 +203,7 @@ describe("inbound invite redemption intercept", () => {
204
203
  expect(deliverReplyCalls.length).toBe(1);
205
204
  const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>)
206
205
  .text;
207
- expect(replyText).toContain(
208
- "Welcome! You've been granted access via invite link.",
209
- );
206
+ expect(replyText).toContain("Welcome! You've been granted access.");
210
207
  });
211
208
 
212
209
  test("non-member with invalid token gets refusal text", async () => {