@vellumai/assistant 0.4.15 → 0.4.17

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 (73) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/package.json +1 -1
  4. package/src/__tests__/approval-routes-http.test.ts +383 -254
  5. package/src/__tests__/call-controller.test.ts +1074 -751
  6. package/src/__tests__/call-routes-http.test.ts +329 -279
  7. package/src/__tests__/channel-approval-routes.test.ts +2 -13
  8. package/src/__tests__/channel-approvals.test.ts +227 -182
  9. package/src/__tests__/channel-guardian.test.ts +1 -0
  10. package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
  11. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  12. package/src/__tests__/conversation-routes.test.ts +71 -41
  13. package/src/__tests__/daemon-server-session-init.test.ts +258 -191
  14. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
  15. package/src/__tests__/extract-email.test.ts +42 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
  17. package/src/__tests__/gateway-only-guard.test.ts +54 -55
  18. package/src/__tests__/gmail-integration.test.ts +48 -46
  19. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
  20. package/src/__tests__/guardian-outbound-http.test.ts +334 -208
  21. package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
  22. package/src/__tests__/guardian-routing-state.test.ts +257 -209
  23. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
  24. package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
  25. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
  26. package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
  27. package/src/__tests__/ingress-reconcile.test.ts +184 -142
  28. package/src/__tests__/non-member-access-request.test.ts +291 -247
  29. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  30. package/src/__tests__/pairing-concurrent.test.ts +78 -0
  31. package/src/__tests__/recording-intent-handler.test.ts +422 -291
  32. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
  33. package/src/__tests__/runtime-events-sse.test.ts +67 -50
  34. package/src/__tests__/send-endpoint-busy.test.ts +314 -232
  35. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  36. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  37. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
  38. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
  39. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
  40. package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
  41. package/src/__tests__/twilio-config.test.ts +49 -41
  42. package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
  43. package/src/__tests__/twilio-routes.test.ts +389 -280
  44. package/src/calls/call-controller.ts +1 -1
  45. package/src/calls/guardian-action-sweep.ts +6 -6
  46. package/src/calls/twilio-routes.ts +2 -4
  47. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
  48. package/src/config/bundled-skills/messaging/SKILL.md +5 -4
  49. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +69 -4
  50. package/src/config/env.ts +39 -29
  51. package/src/daemon/handlers/config-inbox.ts +5 -5
  52. package/src/daemon/handlers/skills.ts +18 -10
  53. package/src/daemon/ipc-contract/messages.ts +1 -0
  54. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  55. package/src/daemon/pairing-store.ts +15 -2
  56. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  57. package/src/daemon/session-agent-loop.ts +1 -1
  58. package/src/daemon/session-process.ts +1 -1
  59. package/src/daemon/session-slash.ts +4 -4
  60. package/src/daemon/session-surfaces.ts +42 -2
  61. package/src/runtime/auth/token-service.ts +95 -45
  62. package/src/runtime/channel-retry-sweep.ts +2 -2
  63. package/src/runtime/http-server.ts +8 -7
  64. package/src/runtime/http-types.ts +1 -1
  65. package/src/runtime/routes/conversation-routes.ts +1 -1
  66. package/src/runtime/routes/guardian-bootstrap-routes.ts +3 -2
  67. package/src/runtime/routes/guardian-expiry-sweep.ts +5 -5
  68. package/src/runtime/routes/pairing-routes.ts +4 -1
  69. package/src/sequence/reply-matcher.ts +14 -4
  70. package/src/skills/frontmatter.ts +9 -6
  71. package/src/tools/ui-surface/definitions.ts +3 -1
  72. package/src/util/platform.ts +0 -12
  73. package/docs/architecture/http-token-refresh.md +0 -274
@@ -9,33 +9,34 @@
9
9
  * 5. Delivery failures allow retry on next poll
10
10
  */
11
11
 
12
- import { mkdtempSync, rmSync } from 'node:fs';
13
- import { tmpdir } from 'node:os';
14
- import { join } from 'node:path';
12
+ import { mkdtempSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
15
 
16
- import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
16
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
17
17
 
18
- const testDir = mkdtempSync(join(tmpdir(), 'tc-approval-notifier-test-'));
18
+ const testDir = mkdtempSync(join(tmpdir(), "tc-approval-notifier-test-"));
19
19
 
20
20
  // ── Platform mock ──
21
- mock.module('../util/platform.js', () => ({
21
+ mock.module("../util/platform.js", () => ({
22
22
  getDataDir: () => testDir,
23
- isMacOS: () => process.platform === 'darwin',
24
- isLinux: () => process.platform === 'linux',
25
- isWindows: () => process.platform === 'win32',
26
- getSocketPath: () => join(testDir, 'test.sock'),
27
- getPidPath: () => join(testDir, 'test.pid'),
28
- getDbPath: () => join(testDir, 'test.db'),
29
- getLogPath: () => join(testDir, 'test.log'),
30
- readHttpToken: () => 'test-token',
23
+ isMacOS: () => process.platform === "darwin",
24
+ isLinux: () => process.platform === "linux",
25
+ isWindows: () => process.platform === "win32",
26
+ getSocketPath: () => join(testDir, "test.sock"),
27
+ getPidPath: () => join(testDir, "test.pid"),
28
+ getDbPath: () => join(testDir, "test.db"),
29
+ getLogPath: () => join(testDir, "test.log"),
30
+ readHttpToken: () => "test-token",
31
31
  ensureDataDir: () => {},
32
32
  migrateToDataLayout: () => {},
33
33
  migrateToWorkspaceLayout: () => {},
34
- normalizeAssistantId: (id: string) => id === 'self' || id === '' ? 'self' : id,
34
+ normalizeAssistantId: (id: string) =>
35
+ id === "self" || id === "" ? "self" : id,
35
36
  }));
36
37
 
37
38
  // ── Logger mock ──
38
- mock.module('../util/logger.js', () => ({
39
+ mock.module("../util/logger.js", () => ({
39
40
  getLogger: () =>
40
41
  new Proxy({} as Record<string, unknown>, {
41
42
  get: () => () => {},
@@ -45,12 +46,12 @@ mock.module('../util/logger.js', () => ({
45
46
  }));
46
47
 
47
48
  // ── Notification signal mock ──
48
- mock.module('../notifications/emit-signal.js', () => ({
49
+ mock.module("../notifications/emit-signal.js", () => ({
49
50
  emitNotificationSignal: async () => ({
50
- signalId: 'test-signal',
51
+ signalId: "test-signal",
51
52
  deduplicated: false,
52
53
  dispatched: true,
53
- reason: 'ok',
54
+ reason: "ok",
54
55
  deliveryResults: [],
55
56
  }),
56
57
  registerBroadcastFn: () => {},
@@ -65,14 +66,14 @@ const deliveredReplies: Array<{
65
66
  }> = [];
66
67
  let deliverShouldFail = false;
67
68
 
68
- mock.module('../runtime/gateway-client.js', () => ({
69
+ mock.module("../runtime/gateway-client.js", () => ({
69
70
  deliverChannelReply: async (
70
71
  url: string,
71
72
  payload: Record<string, unknown>,
72
73
  bearerToken?: string,
73
74
  ) => {
74
75
  if (deliverShouldFail) {
75
- throw new Error('Delivery failed');
76
+ throw new Error("Delivery failed");
76
77
  }
77
78
  deliveredReplies.push({ url, payload, bearerToken });
78
79
  return { ok: true };
@@ -82,7 +83,7 @@ mock.module('../runtime/gateway-client.js', () => ({
82
83
  // ── Guardian binding mock ──
83
84
  let mockGuardianBinding: Record<string, unknown> | null = null;
84
85
 
85
- mock.module('../runtime/channel-guardian-service.js', () => ({
86
+ mock.module("../runtime/channel-guardian-service.js", () => ({
86
87
  getGuardianBinding: () => mockGuardianBinding,
87
88
  // Re-export stubs for other functions to prevent import errors
88
89
  bindSessionIdentity: () => {},
@@ -94,7 +95,10 @@ mock.module('../runtime/channel-guardian-service.js', () => ({
94
95
  resolveBootstrapToken: () => null,
95
96
  updateSessionDelivery: () => {},
96
97
  updateSessionStatus: () => {},
97
- validateAndConsumeChallenge: () => ({ success: false, reason: 'no_challenge' }),
98
+ validateAndConsumeChallenge: () => ({
99
+ success: false,
100
+ reason: "no_challenge",
101
+ }),
98
102
  }));
99
103
 
100
104
  // ── Pending interactions mock ──
@@ -105,20 +109,21 @@ let mockPendingApprovals: Array<{
105
109
  riskLevel: string;
106
110
  }> = [];
107
111
 
108
- mock.module('../runtime/channel-approvals.js', () => ({
112
+ mock.module("../runtime/channel-approvals.js", () => ({
109
113
  getApprovalInfoByConversation: () => mockPendingApprovals,
110
114
  getChannelApprovalPrompt: () => null,
111
115
  buildApprovalUIMetadata: () => ({}),
112
116
  }));
113
117
 
114
118
  // ── Config env mock ──
115
- mock.module('../config/env.js', () => ({
116
- getGatewayInternalBaseUrl: () => 'http://localhost:3000',
119
+ mock.module("../config/env.js", () => ({
120
+ isHttpAuthDisabled: () => true,
121
+ getGatewayInternalBaseUrl: () => "http://localhost:3000",
117
122
  }));
118
123
 
119
124
  // Import module under test AFTER mocks are set up
120
- import type { ChannelId } from '../channels/types.js';
121
- import type { GuardianContext } from '../runtime/guardian-context-resolver.js';
125
+ import type { ChannelId } from "../channels/types.js";
126
+ import type { GuardianContext } from "../runtime/guardian-context-resolver.js";
122
127
 
123
128
  // We need to test the private functions by importing the module.
124
129
  // Since startTrustedContactApprovalNotifier is not exported, we test it
@@ -146,7 +151,7 @@ async function simulateNotifierPoll(params: {
146
151
  conversationId: string;
147
152
  sourceChannel: ChannelId;
148
153
  externalChatId: string;
149
- guardianTrustClass: GuardianContext['trustClass'];
154
+ guardianTrustClass: GuardianContext["trustClass"];
150
155
  guardianExternalUserId?: string;
151
156
  replyCallbackUrl: string;
152
157
  bearerToken?: string;
@@ -161,19 +166,21 @@ async function simulateNotifierPoll(params: {
161
166
  } = params;
162
167
 
163
168
  // Gate check: only trusted contacts with guardian route
164
- if (guardianTrustClass !== 'trusted_contact' || !guardianExternalUserId) {
169
+ if (guardianTrustClass !== "trusted_contact" || !guardianExternalUserId) {
165
170
  return false;
166
171
  }
167
172
 
168
- const { getApprovalInfoByConversation } = await import('../runtime/channel-approvals.js');
169
- const { deliverChannelReply } = await import('../runtime/gateway-client.js');
170
- const { getGuardianBinding } = await import('../runtime/channel-guardian-service.js');
173
+ const { getApprovalInfoByConversation } =
174
+ await import("../runtime/channel-approvals.js");
175
+ const { deliverChannelReply } = await import("../runtime/gateway-client.js");
176
+ const { getGuardianBinding } =
177
+ await import("../runtime/channel-guardian-service.js");
171
178
 
172
179
  const pending = getApprovalInfoByConversation(params.conversationId);
173
180
  const info = pending[0];
174
181
 
175
182
  // Clean up resolved requests — only for THIS conversation's entries.
176
- const currentPendingIds = new Set(pending.map(p => p.requestId));
183
+ const currentPendingIds = new Set(pending.map((p) => p.requestId));
177
184
  for (const [rid, cid] of notifiedRequestIds) {
178
185
  if (cid === conversationId && !currentPendingIds.has(rid)) {
179
186
  notifiedRequestIds.delete(rid);
@@ -188,13 +195,25 @@ async function simulateNotifierPoll(params: {
188
195
 
189
196
  // Resolve guardian name
190
197
  let guardianName: string | undefined;
191
- const binding = getGuardianBinding(params.assistantId ?? 'self', params.sourceChannel);
198
+ const binding = getGuardianBinding(
199
+ params.assistantId ?? "self",
200
+ params.sourceChannel,
201
+ );
192
202
  if (binding?.metadataJson) {
193
203
  try {
194
- const parsed = JSON.parse(binding.metadataJson as string) as Record<string, unknown>;
195
- if (typeof parsed.displayName === 'string' && parsed.displayName.trim().length > 0) {
204
+ const parsed = JSON.parse(binding.metadataJson as string) as Record<
205
+ string,
206
+ unknown
207
+ >;
208
+ if (
209
+ typeof parsed.displayName === "string" &&
210
+ parsed.displayName.trim().length > 0
211
+ ) {
196
212
  guardianName = parsed.displayName.trim();
197
- } else if (typeof parsed.username === 'string' && parsed.username.trim().length > 0) {
213
+ } else if (
214
+ typeof parsed.username === "string" &&
215
+ parsed.username.trim().length > 0
216
+ ) {
198
217
  guardianName = `@${parsed.username.trim()}`;
199
218
  }
200
219
  } catch {
@@ -204,14 +223,18 @@ async function simulateNotifierPoll(params: {
204
223
 
205
224
  const waitingText = guardianName
206
225
  ? `Waiting for ${guardianName}'s approval...`
207
- : 'Waiting for your guardian\'s approval...';
226
+ : "Waiting for your guardian's approval...";
208
227
 
209
228
  try {
210
- await deliverChannelReply(params.replyCallbackUrl, {
211
- chatId: params.externalChatId,
212
- text: waitingText,
213
- assistantId: params.assistantId ?? 'self',
214
- }, params.bearerToken);
229
+ await deliverChannelReply(
230
+ params.replyCallbackUrl,
231
+ {
232
+ chatId: params.externalChatId,
233
+ text: waitingText,
234
+ assistantId: params.assistantId ?? "self",
235
+ },
236
+ params.bearerToken,
237
+ );
215
238
  return true;
216
239
  } catch {
217
240
  notifiedRequestIds.delete(info.requestId);
@@ -223,7 +246,7 @@ async function simulateNotifierPoll(params: {
223
246
  // TESTS
224
247
  // ===========================================================================
225
248
 
226
- describe('trusted-contact pending-approval notifier', () => {
249
+ describe("trusted-contact pending-approval notifier", () => {
227
250
  beforeEach(() => {
228
251
  deliveredReplies.length = 0;
229
252
  deliverShouldFail = false;
@@ -239,142 +262,160 @@ describe('trusted-contact pending-approval notifier', () => {
239
262
  }
240
263
  });
241
264
 
242
- test('sends waiting message to trusted contact when pending approval exists', async () => {
243
- mockPendingApprovals = [{
244
- requestId: 'req-1',
245
- toolName: 'bash',
246
- input: { command: 'ls' },
247
- riskLevel: 'medium',
248
- }];
265
+ test("sends waiting message to trusted contact when pending approval exists", async () => {
266
+ mockPendingApprovals = [
267
+ {
268
+ requestId: "req-1",
269
+ toolName: "bash",
270
+ input: { command: "ls" },
271
+ riskLevel: "medium",
272
+ },
273
+ ];
249
274
 
250
275
  mockGuardianBinding = {
251
- id: 'binding-1',
252
- metadataJson: JSON.stringify({ displayName: 'Mom' }),
276
+ id: "binding-1",
277
+ metadataJson: JSON.stringify({ displayName: "Mom" }),
253
278
  };
254
279
 
255
280
  const notified = new Map<string, string>();
256
281
  const sent = await simulateNotifierPoll({
257
- conversationId: 'conv-1',
258
- sourceChannel: 'telegram',
259
- externalChatId: 'chat-123',
260
- guardianTrustClass: 'trusted_contact',
261
- guardianExternalUserId: 'guardian-1',
262
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
263
- bearerToken: 'test-token',
264
- assistantId: 'self',
282
+ conversationId: "conv-1",
283
+ sourceChannel: "telegram",
284
+ externalChatId: "chat-123",
285
+ guardianTrustClass: "trusted_contact",
286
+ guardianExternalUserId: "guardian-1",
287
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
288
+ bearerToken: "test-token",
289
+ assistantId: "self",
265
290
  notifiedRequestIds: notified,
266
291
  });
267
292
 
268
293
  expect(sent).toBe(true);
269
294
  expect(deliveredReplies).toHaveLength(1);
270
- expect(deliveredReplies[0].payload.text).toBe("Waiting for Mom's approval...");
271
- expect(deliveredReplies[0].payload.chatId).toBe('chat-123');
272
- expect(notified.has('req-1')).toBe(true);
295
+ expect(deliveredReplies[0].payload.text).toBe(
296
+ "Waiting for Mom's approval...",
297
+ );
298
+ expect(deliveredReplies[0].payload.chatId).toBe("chat-123");
299
+ expect(notified.has("req-1")).toBe(true);
273
300
  });
274
301
 
275
- test('uses username with @ prefix when display name is not available', async () => {
276
- mockPendingApprovals = [{
277
- requestId: 'req-2',
278
- toolName: 'bash',
279
- input: {},
280
- riskLevel: 'medium',
281
- }];
302
+ test("uses username with @ prefix when display name is not available", async () => {
303
+ mockPendingApprovals = [
304
+ {
305
+ requestId: "req-2",
306
+ toolName: "bash",
307
+ input: {},
308
+ riskLevel: "medium",
309
+ },
310
+ ];
282
311
 
283
312
  mockGuardianBinding = {
284
- id: 'binding-1',
285
- metadataJson: JSON.stringify({ username: 'guardian_user' }),
313
+ id: "binding-1",
314
+ metadataJson: JSON.stringify({ username: "guardian_user" }),
286
315
  };
287
316
 
288
317
  const notified = new Map<string, string>();
289
318
  await simulateNotifierPoll({
290
- conversationId: 'conv-1',
291
- sourceChannel: 'telegram',
292
- externalChatId: 'chat-123',
293
- guardianTrustClass: 'trusted_contact',
294
- guardianExternalUserId: 'guardian-1',
295
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
319
+ conversationId: "conv-1",
320
+ sourceChannel: "telegram",
321
+ externalChatId: "chat-123",
322
+ guardianTrustClass: "trusted_contact",
323
+ guardianExternalUserId: "guardian-1",
324
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
296
325
  notifiedRequestIds: notified,
297
326
  });
298
327
 
299
328
  expect(deliveredReplies).toHaveLength(1);
300
- expect(deliveredReplies[0].payload.text).toBe("Waiting for @guardian_user's approval...");
329
+ expect(deliveredReplies[0].payload.text).toBe(
330
+ "Waiting for @guardian_user's approval...",
331
+ );
301
332
  });
302
333
 
303
- test('uses generic phrasing when no guardian name is available', async () => {
304
- mockPendingApprovals = [{
305
- requestId: 'req-3',
306
- toolName: 'bash',
307
- input: {},
308
- riskLevel: 'medium',
309
- }];
334
+ test("uses generic phrasing when no guardian name is available", async () => {
335
+ mockPendingApprovals = [
336
+ {
337
+ requestId: "req-3",
338
+ toolName: "bash",
339
+ input: {},
340
+ riskLevel: "medium",
341
+ },
342
+ ];
310
343
 
311
344
  // No binding metadata
312
345
  mockGuardianBinding = {
313
- id: 'binding-1',
346
+ id: "binding-1",
314
347
  metadataJson: null,
315
348
  };
316
349
 
317
350
  const notified = new Map<string, string>();
318
351
  await simulateNotifierPoll({
319
- conversationId: 'conv-1',
320
- sourceChannel: 'telegram',
321
- externalChatId: 'chat-123',
322
- guardianTrustClass: 'trusted_contact',
323
- guardianExternalUserId: 'guardian-1',
324
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
352
+ conversationId: "conv-1",
353
+ sourceChannel: "telegram",
354
+ externalChatId: "chat-123",
355
+ guardianTrustClass: "trusted_contact",
356
+ guardianExternalUserId: "guardian-1",
357
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
325
358
  notifiedRequestIds: notified,
326
359
  });
327
360
 
328
361
  expect(deliveredReplies).toHaveLength(1);
329
- expect(deliveredReplies[0].payload.text).toBe("Waiting for your guardian's approval...");
362
+ expect(deliveredReplies[0].payload.text).toBe(
363
+ "Waiting for your guardian's approval...",
364
+ );
330
365
  });
331
366
 
332
- test('uses generic phrasing when no guardian binding exists', async () => {
333
- mockPendingApprovals = [{
334
- requestId: 'req-4',
335
- toolName: 'bash',
336
- input: {},
337
- riskLevel: 'medium',
338
- }];
367
+ test("uses generic phrasing when no guardian binding exists", async () => {
368
+ mockPendingApprovals = [
369
+ {
370
+ requestId: "req-4",
371
+ toolName: "bash",
372
+ input: {},
373
+ riskLevel: "medium",
374
+ },
375
+ ];
339
376
 
340
377
  mockGuardianBinding = null;
341
378
 
342
379
  const notified = new Map<string, string>();
343
380
  await simulateNotifierPoll({
344
- conversationId: 'conv-1',
345
- sourceChannel: 'telegram',
346
- externalChatId: 'chat-123',
347
- guardianTrustClass: 'trusted_contact',
348
- guardianExternalUserId: 'guardian-1',
349
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
381
+ conversationId: "conv-1",
382
+ sourceChannel: "telegram",
383
+ externalChatId: "chat-123",
384
+ guardianTrustClass: "trusted_contact",
385
+ guardianExternalUserId: "guardian-1",
386
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
350
387
  notifiedRequestIds: notified,
351
388
  });
352
389
 
353
390
  expect(deliveredReplies).toHaveLength(1);
354
- expect(deliveredReplies[0].payload.text).toBe("Waiting for your guardian's approval...");
391
+ expect(deliveredReplies[0].payload.text).toBe(
392
+ "Waiting for your guardian's approval...",
393
+ );
355
394
  });
356
395
 
357
- test('deduplicates by requestId — does not send twice for same request', async () => {
358
- mockPendingApprovals = [{
359
- requestId: 'req-5',
360
- toolName: 'bash',
361
- input: {},
362
- riskLevel: 'medium',
363
- }];
396
+ test("deduplicates by requestId — does not send twice for same request", async () => {
397
+ mockPendingApprovals = [
398
+ {
399
+ requestId: "req-5",
400
+ toolName: "bash",
401
+ input: {},
402
+ riskLevel: "medium",
403
+ },
404
+ ];
364
405
 
365
406
  mockGuardianBinding = {
366
- id: 'binding-1',
367
- metadataJson: JSON.stringify({ displayName: 'Guardian' }),
407
+ id: "binding-1",
408
+ metadataJson: JSON.stringify({ displayName: "Guardian" }),
368
409
  };
369
410
 
370
411
  const notified = new Map<string, string>();
371
412
  const baseParams = {
372
- conversationId: 'conv-1',
373
- sourceChannel: 'telegram' as ChannelId,
374
- externalChatId: 'chat-123',
375
- guardianTrustClass: 'trusted_contact' as const,
376
- guardianExternalUserId: 'guardian-1',
377
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
413
+ conversationId: "conv-1",
414
+ sourceChannel: "telegram" as ChannelId,
415
+ externalChatId: "chat-123",
416
+ guardianTrustClass: "trusted_contact" as const,
417
+ guardianExternalUserId: "guardian-1",
418
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
378
419
  notifiedRequestIds: notified,
379
420
  };
380
421
 
@@ -389,67 +430,73 @@ describe('trusted-contact pending-approval notifier', () => {
389
430
  expect(deliveredReplies).toHaveLength(1); // Still just 1
390
431
  });
391
432
 
392
- test('sends separate messages for different requestIds', async () => {
433
+ test("sends separate messages for different requestIds", async () => {
393
434
  mockGuardianBinding = {
394
- id: 'binding-1',
395
- metadataJson: JSON.stringify({ displayName: 'Guardian' }),
435
+ id: "binding-1",
436
+ metadataJson: JSON.stringify({ displayName: "Guardian" }),
396
437
  };
397
438
 
398
439
  const notified = new Map<string, string>();
399
440
  const baseParams = {
400
- conversationId: 'conv-1',
401
- sourceChannel: 'telegram' as ChannelId,
402
- externalChatId: 'chat-123',
403
- guardianTrustClass: 'trusted_contact' as const,
404
- guardianExternalUserId: 'guardian-1',
405
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
441
+ conversationId: "conv-1",
442
+ sourceChannel: "telegram" as ChannelId,
443
+ externalChatId: "chat-123",
444
+ guardianTrustClass: "trusted_contact" as const,
445
+ guardianExternalUserId: "guardian-1",
446
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
406
447
  notifiedRequestIds: notified,
407
448
  };
408
449
 
409
450
  // First request
410
- mockPendingApprovals = [{
411
- requestId: 'req-A',
412
- toolName: 'bash',
413
- input: {},
414
- riskLevel: 'medium',
415
- }];
451
+ mockPendingApprovals = [
452
+ {
453
+ requestId: "req-A",
454
+ toolName: "bash",
455
+ input: {},
456
+ riskLevel: "medium",
457
+ },
458
+ ];
416
459
  await simulateNotifierPoll(baseParams);
417
460
  expect(deliveredReplies).toHaveLength(1);
418
461
 
419
462
  // Second request (different requestId)
420
- mockPendingApprovals = [{
421
- requestId: 'req-B',
422
- toolName: 'read_file',
423
- input: {},
424
- riskLevel: 'low',
425
- }];
463
+ mockPendingApprovals = [
464
+ {
465
+ requestId: "req-B",
466
+ toolName: "read_file",
467
+ input: {},
468
+ riskLevel: "low",
469
+ },
470
+ ];
426
471
  await simulateNotifierPoll(baseParams);
427
472
  expect(deliveredReplies).toHaveLength(2);
428
473
  });
429
474
 
430
- test('concurrent pollers for different conversations do not evict each other', async () => {
475
+ test("concurrent pollers for different conversations do not evict each other", async () => {
431
476
  mockGuardianBinding = {
432
- id: 'binding-1',
433
- metadataJson: JSON.stringify({ displayName: 'Guardian' }),
477
+ id: "binding-1",
478
+ metadataJson: JSON.stringify({ displayName: "Guardian" }),
434
479
  };
435
480
 
436
481
  // Shared dedupe map simulating the module-level global
437
482
  const notified = new Map<string, string>();
438
483
 
439
484
  // Conversation A gets a pending approval and notifies
440
- mockPendingApprovals = [{
441
- requestId: 'req-convA',
442
- toolName: 'bash',
443
- input: {},
444
- riskLevel: 'medium',
445
- }];
485
+ mockPendingApprovals = [
486
+ {
487
+ requestId: "req-convA",
488
+ toolName: "bash",
489
+ input: {},
490
+ riskLevel: "medium",
491
+ },
492
+ ];
446
493
  const sentA = await simulateNotifierPoll({
447
- conversationId: 'conv-A',
448
- sourceChannel: 'telegram',
449
- externalChatId: 'chat-A',
450
- guardianTrustClass: 'trusted_contact',
451
- guardianExternalUserId: 'guardian-1',
452
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
494
+ conversationId: "conv-A",
495
+ sourceChannel: "telegram",
496
+ externalChatId: "chat-A",
497
+ guardianTrustClass: "trusted_contact",
498
+ guardianExternalUserId: "guardian-1",
499
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
453
500
  notifiedRequestIds: notified,
454
501
  });
455
502
  expect(sentA).toBe(true);
@@ -459,55 +506,59 @@ describe('trusted-contact pending-approval notifier', () => {
459
506
  // NOT evict conv-A's entry from the shared map.
460
507
  mockPendingApprovals = [];
461
508
  await simulateNotifierPoll({
462
- conversationId: 'conv-B',
463
- sourceChannel: 'telegram',
464
- externalChatId: 'chat-B',
465
- guardianTrustClass: 'trusted_contact',
466
- guardianExternalUserId: 'guardian-1',
467
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
509
+ conversationId: "conv-B",
510
+ sourceChannel: "telegram",
511
+ externalChatId: "chat-B",
512
+ guardianTrustClass: "trusted_contact",
513
+ guardianExternalUserId: "guardian-1",
514
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
468
515
  notifiedRequestIds: notified,
469
516
  });
470
517
 
471
518
  // req-convA should still be in the notified map (not evicted by conv-B)
472
- expect(notified.has('req-convA')).toBe(true);
519
+ expect(notified.has("req-convA")).toBe(true);
473
520
 
474
521
  // Re-poll conversation A with the same pending approval — should NOT
475
522
  // re-send because the entry was preserved.
476
- mockPendingApprovals = [{
477
- requestId: 'req-convA',
478
- toolName: 'bash',
479
- input: {},
480
- riskLevel: 'medium',
481
- }];
523
+ mockPendingApprovals = [
524
+ {
525
+ requestId: "req-convA",
526
+ toolName: "bash",
527
+ input: {},
528
+ riskLevel: "medium",
529
+ },
530
+ ];
482
531
  const sentA2 = await simulateNotifierPoll({
483
- conversationId: 'conv-A',
484
- sourceChannel: 'telegram',
485
- externalChatId: 'chat-A',
486
- guardianTrustClass: 'trusted_contact',
487
- guardianExternalUserId: 'guardian-1',
488
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
532
+ conversationId: "conv-A",
533
+ sourceChannel: "telegram",
534
+ externalChatId: "chat-A",
535
+ guardianTrustClass: "trusted_contact",
536
+ guardianExternalUserId: "guardian-1",
537
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
489
538
  notifiedRequestIds: notified,
490
539
  });
491
540
  expect(sentA2).toBe(false);
492
541
  expect(deliveredReplies).toHaveLength(1); // Still just 1 — no duplicate
493
542
  });
494
543
 
495
- test('does not activate for guardian actors', async () => {
496
- mockPendingApprovals = [{
497
- requestId: 'req-6',
498
- toolName: 'bash',
499
- input: {},
500
- riskLevel: 'medium',
501
- }];
544
+ test("does not activate for guardian actors", async () => {
545
+ mockPendingApprovals = [
546
+ {
547
+ requestId: "req-6",
548
+ toolName: "bash",
549
+ input: {},
550
+ riskLevel: "medium",
551
+ },
552
+ ];
502
553
 
503
554
  const notified = new Map<string, string>();
504
555
  const sent = await simulateNotifierPoll({
505
- conversationId: 'conv-1',
506
- sourceChannel: 'telegram',
507
- externalChatId: 'chat-123',
508
- guardianTrustClass: 'guardian',
509
- guardianExternalUserId: 'guardian-1',
510
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
556
+ conversationId: "conv-1",
557
+ sourceChannel: "telegram",
558
+ externalChatId: "chat-123",
559
+ guardianTrustClass: "guardian",
560
+ guardianExternalUserId: "guardian-1",
561
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
511
562
  notifiedRequestIds: notified,
512
563
  });
513
564
 
@@ -515,21 +566,23 @@ describe('trusted-contact pending-approval notifier', () => {
515
566
  expect(deliveredReplies).toHaveLength(0);
516
567
  });
517
568
 
518
- test('does not activate for unknown actors', async () => {
519
- mockPendingApprovals = [{
520
- requestId: 'req-7',
521
- toolName: 'bash',
522
- input: {},
523
- riskLevel: 'medium',
524
- }];
569
+ test("does not activate for unknown actors", async () => {
570
+ mockPendingApprovals = [
571
+ {
572
+ requestId: "req-7",
573
+ toolName: "bash",
574
+ input: {},
575
+ riskLevel: "medium",
576
+ },
577
+ ];
525
578
 
526
579
  const notified = new Map<string, string>();
527
580
  const sent = await simulateNotifierPoll({
528
- conversationId: 'conv-1',
529
- sourceChannel: 'telegram',
530
- externalChatId: 'chat-123',
531
- guardianTrustClass: 'unknown',
532
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
581
+ conversationId: "conv-1",
582
+ sourceChannel: "telegram",
583
+ externalChatId: "chat-123",
584
+ guardianTrustClass: "unknown",
585
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
533
586
  notifiedRequestIds: notified,
534
587
  });
535
588
 
@@ -537,22 +590,24 @@ describe('trusted-contact pending-approval notifier', () => {
537
590
  expect(deliveredReplies).toHaveLength(0);
538
591
  });
539
592
 
540
- test('does not activate for trusted contact without guardian identity', async () => {
541
- mockPendingApprovals = [{
542
- requestId: 'req-8',
543
- toolName: 'bash',
544
- input: {},
545
- riskLevel: 'medium',
546
- }];
593
+ test("does not activate for trusted contact without guardian identity", async () => {
594
+ mockPendingApprovals = [
595
+ {
596
+ requestId: "req-8",
597
+ toolName: "bash",
598
+ input: {},
599
+ riskLevel: "medium",
600
+ },
601
+ ];
547
602
 
548
603
  const notified = new Map<string, string>();
549
604
  const sent = await simulateNotifierPoll({
550
- conversationId: 'conv-1',
551
- sourceChannel: 'telegram',
552
- externalChatId: 'chat-123',
553
- guardianTrustClass: 'trusted_contact',
605
+ conversationId: "conv-1",
606
+ sourceChannel: "telegram",
607
+ externalChatId: "chat-123",
608
+ guardianTrustClass: "trusted_contact",
554
609
  guardianExternalUserId: undefined,
555
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
610
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
556
611
  notifiedRequestIds: notified,
557
612
  });
558
613
 
@@ -560,27 +615,29 @@ describe('trusted-contact pending-approval notifier', () => {
560
615
  expect(deliveredReplies).toHaveLength(0);
561
616
  });
562
617
 
563
- test('retries delivery on failure — removes requestId from notified set', async () => {
564
- mockPendingApprovals = [{
565
- requestId: 'req-9',
566
- toolName: 'bash',
567
- input: {},
568
- riskLevel: 'medium',
569
- }];
618
+ test("retries delivery on failure — removes requestId from notified set", async () => {
619
+ mockPendingApprovals = [
620
+ {
621
+ requestId: "req-9",
622
+ toolName: "bash",
623
+ input: {},
624
+ riskLevel: "medium",
625
+ },
626
+ ];
570
627
 
571
628
  mockGuardianBinding = {
572
- id: 'binding-1',
573
- metadataJson: JSON.stringify({ displayName: 'Guardian' }),
629
+ id: "binding-1",
630
+ metadataJson: JSON.stringify({ displayName: "Guardian" }),
574
631
  };
575
632
 
576
633
  const notified = new Map<string, string>();
577
634
  const baseParams = {
578
- conversationId: 'conv-1',
579
- sourceChannel: 'telegram' as ChannelId,
580
- externalChatId: 'chat-123',
581
- guardianTrustClass: 'trusted_contact' as const,
582
- guardianExternalUserId: 'guardian-1',
583
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
635
+ conversationId: "conv-1",
636
+ sourceChannel: "telegram" as ChannelId,
637
+ externalChatId: "chat-123",
638
+ guardianTrustClass: "trusted_contact" as const,
639
+ guardianExternalUserId: "guardian-1",
640
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
584
641
  notifiedRequestIds: notified,
585
642
  };
586
643
 
@@ -588,27 +645,27 @@ describe('trusted-contact pending-approval notifier', () => {
588
645
  deliverShouldFail = true;
589
646
  const sent1 = await simulateNotifierPoll(baseParams);
590
647
  expect(sent1).toBe(false);
591
- expect(notified.has('req-9')).toBe(false); // Removed for retry
648
+ expect(notified.has("req-9")).toBe(false); // Removed for retry
592
649
 
593
650
  // Second attempt: delivery succeeds
594
651
  deliverShouldFail = false;
595
652
  const sent2 = await simulateNotifierPoll(baseParams);
596
653
  expect(sent2).toBe(true);
597
654
  expect(deliveredReplies).toHaveLength(1);
598
- expect(notified.has('req-9')).toBe(true);
655
+ expect(notified.has("req-9")).toBe(true);
599
656
  });
600
657
 
601
- test('does not send when no pending approvals exist', async () => {
658
+ test("does not send when no pending approvals exist", async () => {
602
659
  mockPendingApprovals = [];
603
660
 
604
661
  const notified = new Map<string, string>();
605
662
  const sent = await simulateNotifierPoll({
606
- conversationId: 'conv-1',
607
- sourceChannel: 'telegram',
608
- externalChatId: 'chat-123',
609
- guardianTrustClass: 'trusted_contact',
610
- guardianExternalUserId: 'guardian-1',
611
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
663
+ conversationId: "conv-1",
664
+ sourceChannel: "telegram",
665
+ externalChatId: "chat-123",
666
+ guardianTrustClass: "trusted_contact",
667
+ guardianExternalUserId: "guardian-1",
668
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
612
669
  notifiedRequestIds: notified,
613
670
  });
614
671
 
@@ -616,63 +673,71 @@ describe('trusted-contact pending-approval notifier', () => {
616
673
  expect(deliveredReplies).toHaveLength(0);
617
674
  });
618
675
 
619
- test('prefers displayName over username when both are present', async () => {
620
- mockPendingApprovals = [{
621
- requestId: 'req-10',
622
- toolName: 'bash',
623
- input: {},
624
- riskLevel: 'medium',
625
- }];
676
+ test("prefers displayName over username when both are present", async () => {
677
+ mockPendingApprovals = [
678
+ {
679
+ requestId: "req-10",
680
+ toolName: "bash",
681
+ input: {},
682
+ riskLevel: "medium",
683
+ },
684
+ ];
626
685
 
627
686
  mockGuardianBinding = {
628
- id: 'binding-1',
687
+ id: "binding-1",
629
688
  metadataJson: JSON.stringify({
630
- displayName: 'Sarah',
631
- username: 'sarah_bot',
689
+ displayName: "Sarah",
690
+ username: "sarah_bot",
632
691
  }),
633
692
  };
634
693
 
635
694
  const notified = new Map<string, string>();
636
695
  await simulateNotifierPoll({
637
- conversationId: 'conv-1',
638
- sourceChannel: 'telegram',
639
- externalChatId: 'chat-123',
640
- guardianTrustClass: 'trusted_contact',
641
- guardianExternalUserId: 'guardian-1',
642
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
696
+ conversationId: "conv-1",
697
+ sourceChannel: "telegram",
698
+ externalChatId: "chat-123",
699
+ guardianTrustClass: "trusted_contact",
700
+ guardianExternalUserId: "guardian-1",
701
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
643
702
  notifiedRequestIds: notified,
644
703
  });
645
704
 
646
705
  expect(deliveredReplies).toHaveLength(1);
647
- expect(deliveredReplies[0].payload.text).toBe("Waiting for Sarah's approval...");
706
+ expect(deliveredReplies[0].payload.text).toBe(
707
+ "Waiting for Sarah's approval...",
708
+ );
648
709
  });
649
710
 
650
- test('handles malformed metadataJson gracefully', async () => {
651
- mockPendingApprovals = [{
652
- requestId: 'req-11',
653
- toolName: 'bash',
654
- input: {},
655
- riskLevel: 'medium',
656
- }];
711
+ test("handles malformed metadataJson gracefully", async () => {
712
+ mockPendingApprovals = [
713
+ {
714
+ requestId: "req-11",
715
+ toolName: "bash",
716
+ input: {},
717
+ riskLevel: "medium",
718
+ },
719
+ ];
657
720
 
658
721
  mockGuardianBinding = {
659
- id: 'binding-1',
660
- metadataJson: 'not-valid-json{{{',
722
+ id: "binding-1",
723
+ metadataJson: "not-valid-json{{{",
661
724
  };
662
725
 
663
726
  const notified = new Map<string, string>();
664
727
  await simulateNotifierPoll({
665
- conversationId: 'conv-1',
666
- sourceChannel: 'telegram',
667
- externalChatId: 'chat-123',
668
- guardianTrustClass: 'trusted_contact',
669
- guardianExternalUserId: 'guardian-1',
670
- replyCallbackUrl: 'http://localhost:3000/deliver/telegram',
728
+ conversationId: "conv-1",
729
+ sourceChannel: "telegram",
730
+ externalChatId: "chat-123",
731
+ guardianTrustClass: "trusted_contact",
732
+ guardianExternalUserId: "guardian-1",
733
+ replyCallbackUrl: "http://localhost:3000/deliver/telegram",
671
734
  notifiedRequestIds: notified,
672
735
  });
673
736
 
674
737
  expect(deliveredReplies).toHaveLength(1);
675
738
  // Falls back to generic phrasing
676
- expect(deliveredReplies[0].payload.text).toBe("Waiting for your guardian's approval...");
739
+ expect(deliveredReplies[0].payload.text).toBe(
740
+ "Waiting for your guardian's approval...",
741
+ );
677
742
  });
678
743
  });