@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
@@ -1,56 +1,59 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
4
 
5
- import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
6
- import { eq } from 'drizzle-orm';
5
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
6
+
7
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
8
+ import { eq } from "drizzle-orm";
7
9
 
8
10
  // ---------------------------------------------------------------------------
9
11
  // Test isolation: in-memory SQLite via temp directory
10
12
  // ---------------------------------------------------------------------------
11
13
 
12
- const testDir = mkdtempSync(join(tmpdir(), 'guardian-routing-state-test-'));
14
+ const testDir = mkdtempSync(join(tmpdir(), "guardian-routing-state-test-"));
13
15
 
14
- mock.module('../util/platform.js', () => ({
16
+ mock.module("../util/platform.js", () => ({
15
17
  getRootDir: () => testDir,
16
18
  getDataDir: () => testDir,
17
- isMacOS: () => process.platform === 'darwin',
18
- isLinux: () => process.platform === 'linux',
19
- isWindows: () => process.platform === 'win32',
20
- getSocketPath: () => join(testDir, 'test.sock'),
21
- getPidPath: () => join(testDir, 'test.pid'),
22
- getDbPath: () => join(testDir, 'test.db'),
23
- getLogPath: () => join(testDir, 'test.log'),
19
+ isMacOS: () => process.platform === "darwin",
20
+ isLinux: () => process.platform === "linux",
21
+ isWindows: () => process.platform === "win32",
22
+ getSocketPath: () => join(testDir, "test.sock"),
23
+ getPidPath: () => join(testDir, "test.pid"),
24
+ getDbPath: () => join(testDir, "test.db"),
25
+ getLogPath: () => join(testDir, "test.log"),
24
26
  ensureDataDir: () => {},
25
27
  }));
26
28
 
27
- mock.module('../util/logger.js', () => ({
28
- getLogger: () => new Proxy({} as Record<string, unknown>, {
29
- get: () => () => {},
30
- }),
29
+ mock.module("../util/logger.js", () => ({
30
+ getLogger: () =>
31
+ new Proxy({} as Record<string, unknown>, {
32
+ get: () => () => {},
33
+ }),
31
34
  }));
32
35
 
33
36
  // Mock security check to always pass
34
- mock.module('../security/secret-ingress.js', () => ({
37
+ mock.module("../security/secret-ingress.js", () => ({
35
38
  checkIngressForSecrets: () => ({ blocked: false }),
36
39
  }));
37
40
 
38
41
  // Mock ingress member store with a configurable member lookup.
39
42
  // By default returns an active member so ACL passes.
40
43
  let mockFindMember: (() => unknown) | null = null;
41
- mock.module('../memory/ingress-member-store.js', () => ({
44
+ mock.module("../memory/ingress-member-store.js", () => ({
42
45
  findMember: (..._args: unknown[]) => {
43
46
  if (mockFindMember) return mockFindMember();
44
47
  return {
45
- id: 'member-test-default',
46
- assistantId: 'self',
47
- sourceChannel: 'telegram',
48
- externalUserId: 'telegram-user-default',
48
+ id: "member-test-default",
49
+ assistantId: "self",
50
+ sourceChannel: "telegram",
51
+ externalUserId: "telegram-user-default",
49
52
  externalChatId: null,
50
53
  displayName: null,
51
54
  username: null,
52
- status: 'active',
53
- policy: 'allow',
55
+ status: "active",
56
+ policy: "allow",
54
57
  inviteId: null,
55
58
  createdBySessionId: null,
56
59
  revokedReason: null,
@@ -64,49 +67,53 @@ mock.module('../memory/ingress-member-store.js', () => ({
64
67
  upsertMember: () => {},
65
68
  }));
66
69
 
67
- import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
68
- import { createBinding } from '../memory/channel-guardian-store.js';
69
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
70
- import { channelInboundEvents, messages } from '../memory/schema.js';
71
- import { sweepFailedEvents } from '../runtime/channel-retry-sweep.js';
70
+ import * as channelDeliveryStore from "../memory/channel-delivery-store.js";
71
+ import { createBinding } from "../memory/channel-guardian-store.js";
72
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
73
+ import { channelInboundEvents, messages } from "../memory/schema.js";
74
+ import { sweepFailedEvents } from "../runtime/channel-retry-sweep.js";
72
75
  import {
73
76
  type GuardianContext,
74
77
  resolveRoutingState,
75
78
  resolveRoutingStateFromRuntime,
76
- } from '../runtime/guardian-context-resolver.js';
77
- import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
79
+ } from "../runtime/guardian-context-resolver.js";
80
+ import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
78
81
 
79
82
  initializeDb();
80
83
 
81
84
  afterAll(() => {
82
85
  resetDb();
83
- try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
86
+ try {
87
+ rmSync(testDir, { recursive: true });
88
+ } catch {
89
+ /* best effort */
90
+ }
84
91
  });
85
92
 
86
93
  function resetTables(): void {
87
94
  const db = getDb();
88
- db.run('DELETE FROM channel_inbound_events');
89
- db.run('DELETE FROM channel_guardian_bindings');
90
- db.run('DELETE FROM channel_guardian_approval_requests');
91
- db.run('DELETE FROM canonical_guardian_requests');
92
- db.run('DELETE FROM conversation_keys');
93
- db.run('DELETE FROM messages');
94
- db.run('DELETE FROM conversations');
95
- db.run('DELETE FROM assistant_ingress_members');
96
- db.run('DELETE FROM external_conversation_bindings');
95
+ db.run("DELETE FROM channel_inbound_events");
96
+ db.run("DELETE FROM channel_guardian_bindings");
97
+ db.run("DELETE FROM channel_guardian_approval_requests");
98
+ db.run("DELETE FROM canonical_guardian_requests");
99
+ db.run("DELETE FROM conversation_keys");
100
+ db.run("DELETE FROM messages");
101
+ db.run("DELETE FROM conversations");
102
+ db.run("DELETE FROM assistant_ingress_members");
103
+ db.run("DELETE FROM external_conversation_bindings");
97
104
  }
98
105
 
99
106
  // ═══════════════════════════════════════════════════════════════════════════
100
107
  // Unit tests: resolveRoutingState
101
108
  // ═══════════════════════════════════════════════════════════════════════════
102
109
 
103
- describe('resolveRoutingState', () => {
104
- test('guardian actors are always interactive and route-resolvable', () => {
110
+ describe("resolveRoutingState", () => {
111
+ test("guardian actors are always interactive and route-resolvable", () => {
105
112
  const ctx: GuardianContext = {
106
- sourceChannel: 'telegram',
107
- trustClass: 'guardian',
108
- guardianExternalUserId: 'guardian-123',
109
- guardianChatId: 'chat-123',
113
+ sourceChannel: "telegram",
114
+ trustClass: "guardian",
115
+ guardianExternalUserId: "guardian-123",
116
+ guardianChatId: "chat-123",
110
117
  };
111
118
  const state = resolveRoutingState(ctx);
112
119
  expect(state).toEqual({
@@ -116,23 +123,23 @@ describe('resolveRoutingState', () => {
116
123
  });
117
124
  });
118
125
 
119
- test('guardian actors are interactive even without guardianExternalUserId', () => {
126
+ test("guardian actors are interactive even without guardianExternalUserId", () => {
120
127
  // Edge case: guardian is chatting in their own chat, no separate binding needed
121
128
  const ctx: GuardianContext = {
122
- sourceChannel: 'telegram',
123
- trustClass: 'guardian',
129
+ sourceChannel: "telegram",
130
+ trustClass: "guardian",
124
131
  };
125
132
  const state = resolveRoutingState(ctx);
126
133
  expect(state.canBeInteractive).toBe(true);
127
134
  expect(state.promptWaitingAllowed).toBe(true);
128
135
  });
129
136
 
130
- test('trusted contact with resolvable guardian route is interactive', () => {
137
+ test("trusted contact with resolvable guardian route is interactive", () => {
131
138
  const ctx: GuardianContext = {
132
- sourceChannel: 'telegram',
133
- trustClass: 'trusted_contact',
134
- guardianExternalUserId: 'guardian-456',
135
- guardianChatId: 'guardian-chat-456',
139
+ sourceChannel: "telegram",
140
+ trustClass: "trusted_contact",
141
+ guardianExternalUserId: "guardian-456",
142
+ guardianChatId: "guardian-chat-456",
136
143
  };
137
144
  const state = resolveRoutingState(ctx);
138
145
  expect(state).toEqual({
@@ -142,10 +149,10 @@ describe('resolveRoutingState', () => {
142
149
  });
143
150
  });
144
151
 
145
- test('trusted contact without guardian route is NOT interactive (fail-fast)', () => {
152
+ test("trusted contact without guardian route is NOT interactive (fail-fast)", () => {
146
153
  const ctx: GuardianContext = {
147
- sourceChannel: 'telegram',
148
- trustClass: 'trusted_contact',
154
+ sourceChannel: "telegram",
155
+ trustClass: "trusted_contact",
149
156
  // No guardianExternalUserId — no guardian binding for this channel
150
157
  };
151
158
  const state = resolveRoutingState(ctx);
@@ -156,15 +163,15 @@ describe('resolveRoutingState', () => {
156
163
  });
157
164
  });
158
165
 
159
- test('unknown actors are never interactive regardless of guardian route', () => {
166
+ test("unknown actors are never interactive regardless of guardian route", () => {
160
167
  const withRoute: GuardianContext = {
161
- sourceChannel: 'telegram',
162
- trustClass: 'unknown',
163
- guardianExternalUserId: 'guardian-789',
168
+ sourceChannel: "telegram",
169
+ trustClass: "unknown",
170
+ guardianExternalUserId: "guardian-789",
164
171
  };
165
172
  const withoutRoute: GuardianContext = {
166
- sourceChannel: 'telegram',
167
- trustClass: 'unknown',
173
+ sourceChannel: "telegram",
174
+ trustClass: "unknown",
168
175
  };
169
176
 
170
177
  expect(resolveRoutingState(withRoute).promptWaitingAllowed).toBe(false);
@@ -173,22 +180,22 @@ describe('resolveRoutingState', () => {
173
180
  });
174
181
  });
175
182
 
176
- describe('resolveRoutingStateFromRuntime', () => {
177
- test('produces same result as resolveRoutingState for guardian runtime context', () => {
183
+ describe("resolveRoutingStateFromRuntime", () => {
184
+ test("produces same result as resolveRoutingState for guardian runtime context", () => {
178
185
  const runtimeCtx = {
179
- sourceChannel: 'telegram' as const,
180
- trustClass: 'trusted_contact' as const,
181
- guardianExternalUserId: 'guardian-rt-1',
186
+ sourceChannel: "telegram" as const,
187
+ trustClass: "trusted_contact" as const,
188
+ guardianExternalUserId: "guardian-rt-1",
182
189
  };
183
190
  const state = resolveRoutingStateFromRuntime(runtimeCtx);
184
191
  expect(state.promptWaitingAllowed).toBe(true);
185
192
  expect(state.guardianRouteResolvable).toBe(true);
186
193
  });
187
194
 
188
- test('trusted contact runtime context without guardian binding is not interactive', () => {
195
+ test("trusted contact runtime context without guardian binding is not interactive", () => {
189
196
  const runtimeCtx = {
190
- sourceChannel: 'telegram' as const,
191
- trustClass: 'trusted_contact' as const,
197
+ sourceChannel: "telegram" as const,
198
+ trustClass: "trusted_contact" as const,
192
199
  // No guardianExternalUserId
193
200
  };
194
201
  const state = resolveRoutingStateFromRuntime(runtimeCtx);
@@ -201,68 +208,78 @@ describe('resolveRoutingStateFromRuntime', () => {
201
208
  // Integration tests: inbound message handler interactivity
202
209
  // ═══════════════════════════════════════════════════════════════════════════
203
210
 
204
- describe('inbound-message-handler trusted-contact interactivity', () => {
211
+ describe("inbound-message-handler trusted-contact interactivity", () => {
205
212
  beforeEach(() => {
206
213
  resetTables();
207
214
  mockFindMember = null;
208
215
  });
209
216
 
210
- function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
211
- return new Request('http://localhost/channels/inbound', {
212
- method: 'POST',
217
+ function makeInboundRequest(
218
+ overrides: Record<string, unknown> = {},
219
+ ): Request {
220
+ return new Request("http://localhost/channels/inbound", {
221
+ method: "POST",
213
222
  headers: {
214
- 'Content-Type': 'application/json',
215
- 'X-Gateway-Origin': 'test-token',
223
+ "Content-Type": "application/json",
224
+ "X-Gateway-Origin": "test-token",
216
225
  },
217
226
  body: JSON.stringify({
218
- sourceChannel: 'telegram',
219
- interface: 'telegram',
220
- conversationExternalId: 'chat-123',
227
+ sourceChannel: "telegram",
228
+ interface: "telegram",
229
+ conversationExternalId: "chat-123",
221
230
  externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
222
- content: 'hello',
223
- actorExternalId: 'telegram-user-default',
224
- replyCallbackUrl: 'https://gateway.test/deliver/telegram',
231
+ content: "hello",
232
+ actorExternalId: "telegram-user-default",
233
+ replyCallbackUrl: "https://gateway.test/deliver/telegram",
225
234
  ...overrides,
226
235
  }),
227
236
  });
228
237
  }
229
238
 
230
- test('trusted contact with guardian binding gets interactive turn', async () => {
239
+ test("trusted contact with guardian binding gets interactive turn", async () => {
231
240
  // Create guardian binding so the trusted contact has a resolvable route
232
241
  createBinding({
233
- assistantId: 'self',
234
- channel: 'telegram',
235
- guardianExternalUserId: 'guardian-user-for-tc',
236
- guardianDeliveryChatId: 'guardian-chat-for-tc',
237
- guardianPrincipalId: 'guardian-user-for-tc',
242
+ assistantId: "self",
243
+ channel: "telegram",
244
+ guardianExternalUserId: "guardian-user-for-tc",
245
+ guardianDeliveryChatId: "guardian-chat-for-tc",
246
+ guardianPrincipalId: "guardian-user-for-tc",
238
247
  });
239
248
 
240
249
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
241
- const processMessage = mock(async (
242
- conversationId: string,
243
- _content: string,
244
- _attachmentIds?: string[],
245
- options?: Record<string, unknown>,
246
- ) => {
247
- processCalls.push({ options });
248
- const messageId = `msg-tc-interactive-${Date.now()}`;
249
- const db = getDb();
250
- db.insert(messages).values({
251
- id: messageId,
252
- conversationId,
253
- role: 'user',
254
- content: JSON.stringify([{ type: 'text', text: 'hello' }]),
255
- createdAt: Date.now(),
256
- }).run();
257
- return { messageId };
258
- });
250
+ const processMessage = mock(
251
+ async (
252
+ conversationId: string,
253
+ _content: string,
254
+ _attachmentIds?: string[],
255
+ options?: Record<string, unknown>,
256
+ ) => {
257
+ processCalls.push({ options });
258
+ const messageId = `msg-tc-interactive-${Date.now()}`;
259
+ const db = getDb();
260
+ db.insert(messages)
261
+ .values({
262
+ id: messageId,
263
+ conversationId,
264
+ role: "user",
265
+ content: JSON.stringify([{ type: "text", text: "hello" }]),
266
+ createdAt: Date.now(),
267
+ })
268
+ .run();
269
+ return { messageId };
270
+ },
271
+ );
259
272
 
260
273
  const req = makeInboundRequest({
261
274
  externalMessageId: `msg-tc-interactive-${Date.now()}`,
262
275
  });
263
276
 
264
- const res = await handleChannelInbound(req, processMessage as any, 'test-token');
265
- const body = await res.json() as Record<string, unknown>;
277
+ const res = await handleChannelInbound(
278
+ req,
279
+ processMessage as any,
280
+ "test-token",
281
+ );
282
+ const body = (await res.json()) as Record<string, unknown>;
266
283
  expect(body.accepted).toBe(true);
267
284
 
268
285
  // Wait for background processing
@@ -272,36 +289,44 @@ describe('inbound-message-handler trusted-contact interactivity', () => {
272
289
  expect(processCalls[0].options?.isInteractive).toBe(true);
273
290
  });
274
291
 
275
- test('trusted contact WITHOUT guardian binding gets non-interactive turn (fail-fast)', async () => {
292
+ test("trusted contact WITHOUT guardian binding gets non-interactive turn (fail-fast)", async () => {
276
293
  // No guardian binding created — trusted contact has no guardian route
277
294
  // but findMember still returns an active member (trusted_contact trust class)
278
295
 
279
296
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
280
- const processMessage = mock(async (
281
- conversationId: string,
282
- _content: string,
283
- _attachmentIds?: string[],
284
- options?: Record<string, unknown>,
285
- ) => {
286
- processCalls.push({ options });
287
- const messageId = `msg-tc-noroute-${Date.now()}`;
288
- const db = getDb();
289
- db.insert(messages).values({
290
- id: messageId,
291
- conversationId,
292
- role: 'user',
293
- content: JSON.stringify([{ type: 'text', text: 'hello' }]),
294
- createdAt: Date.now(),
295
- }).run();
296
- return { messageId };
297
- });
297
+ const processMessage = mock(
298
+ async (
299
+ conversationId: string,
300
+ _content: string,
301
+ _attachmentIds?: string[],
302
+ options?: Record<string, unknown>,
303
+ ) => {
304
+ processCalls.push({ options });
305
+ const messageId = `msg-tc-noroute-${Date.now()}`;
306
+ const db = getDb();
307
+ db.insert(messages)
308
+ .values({
309
+ id: messageId,
310
+ conversationId,
311
+ role: "user",
312
+ content: JSON.stringify([{ type: "text", text: "hello" }]),
313
+ createdAt: Date.now(),
314
+ })
315
+ .run();
316
+ return { messageId };
317
+ },
318
+ );
298
319
 
299
320
  const req = makeInboundRequest({
300
321
  externalMessageId: `msg-tc-noroute-${Date.now()}`,
301
322
  });
302
323
 
303
- const res = await handleChannelInbound(req, processMessage as any, 'test-token');
304
- const body = await res.json() as Record<string, unknown>;
324
+ const res = await handleChannelInbound(
325
+ req,
326
+ processMessage as any,
327
+ "test-token",
328
+ );
329
+ const body = (await res.json()) as Record<string, unknown>;
305
330
  expect(body.accepted).toBe(true);
306
331
 
307
332
  await new Promise((resolve) => setTimeout(resolve, 300));
@@ -312,42 +337,50 @@ describe('inbound-message-handler trusted-contact interactivity', () => {
312
337
  expect(processCalls[0].options?.isInteractive).toBe(false);
313
338
  });
314
339
 
315
- test('guardian actors remain interactive regardless', async () => {
340
+ test("guardian actors remain interactive regardless", async () => {
316
341
  // Guardian binding matches the sender
317
342
  createBinding({
318
- assistantId: 'self',
319
- channel: 'telegram',
320
- guardianExternalUserId: 'telegram-user-default',
321
- guardianDeliveryChatId: 'chat-123',
322
- guardianPrincipalId: 'telegram-user-default',
343
+ assistantId: "self",
344
+ channel: "telegram",
345
+ guardianExternalUserId: "telegram-user-default",
346
+ guardianDeliveryChatId: "chat-123",
347
+ guardianPrincipalId: "telegram-user-default",
323
348
  });
324
349
 
325
350
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
326
- const processMessage = mock(async (
327
- conversationId: string,
328
- _content: string,
329
- _attachmentIds?: string[],
330
- options?: Record<string, unknown>,
331
- ) => {
332
- processCalls.push({ options });
333
- const messageId = `msg-guardian-${Date.now()}`;
334
- const db = getDb();
335
- db.insert(messages).values({
336
- id: messageId,
337
- conversationId,
338
- role: 'user',
339
- content: JSON.stringify([{ type: 'text', text: 'hello' }]),
340
- createdAt: Date.now(),
341
- }).run();
342
- return { messageId };
343
- });
351
+ const processMessage = mock(
352
+ async (
353
+ conversationId: string,
354
+ _content: string,
355
+ _attachmentIds?: string[],
356
+ options?: Record<string, unknown>,
357
+ ) => {
358
+ processCalls.push({ options });
359
+ const messageId = `msg-guardian-${Date.now()}`;
360
+ const db = getDb();
361
+ db.insert(messages)
362
+ .values({
363
+ id: messageId,
364
+ conversationId,
365
+ role: "user",
366
+ content: JSON.stringify([{ type: "text", text: "hello" }]),
367
+ createdAt: Date.now(),
368
+ })
369
+ .run();
370
+ return { messageId };
371
+ },
372
+ );
344
373
 
345
374
  const req = makeInboundRequest({
346
375
  externalMessageId: `msg-guardian-${Date.now()}`,
347
376
  });
348
377
 
349
- const res = await handleChannelInbound(req, processMessage as any, 'test-token');
350
- const body = await res.json() as Record<string, unknown>;
378
+ const res = await handleChannelInbound(
379
+ req,
380
+ processMessage as any,
381
+ "test-token",
382
+ );
383
+ const body = (await res.json()) as Record<string, unknown>;
351
384
  expect(body.accepted).toBe(true);
352
385
 
353
386
  await new Promise((resolve) => setTimeout(resolve, 300));
@@ -356,22 +389,22 @@ describe('inbound-message-handler trusted-contact interactivity', () => {
356
389
  expect(processCalls[0].options?.isInteractive).toBe(true);
357
390
  });
358
391
 
359
- test('unknown actors remain non-interactive (denied at gate)', async () => {
392
+ test("unknown actors remain non-interactive (denied at gate)", async () => {
360
393
  // No member record => non-member denied at the ACL gate,
361
394
  // which is the strongest form of "not interactive".
362
395
  mockFindMember = () => null;
363
396
 
364
397
  const req = makeInboundRequest({
365
398
  externalMessageId: `msg-unknown-${Date.now()}`,
366
- actorExternalId: 'unknown-user-no-member',
399
+ actorExternalId: "unknown-user-no-member",
367
400
  });
368
401
 
369
- const res = await handleChannelInbound(req, undefined, 'test-token');
370
- const body = await res.json() as Record<string, unknown>;
402
+ const res = await handleChannelInbound(req, undefined, "test-token");
403
+ const body = (await res.json()) as Record<string, unknown>;
371
404
  // Unknown actors are ACL-denied: accepted but denied with reason
372
405
  expect(body.accepted).toBe(true);
373
406
  expect(body.denied).toBe(true);
374
- expect(body.reason).toBe('not_a_member');
407
+ expect(body.reason).toBe("not_a_member");
375
408
  });
376
409
  });
377
410
 
@@ -379,22 +412,29 @@ describe('inbound-message-handler trusted-contact interactivity', () => {
379
412
  // Integration tests: channel-retry-sweep routing state
380
413
  // ═══════════════════════════════════════════════════════════════════════════
381
414
 
382
- describe('channel-retry-sweep routing state', () => {
415
+ describe("channel-retry-sweep routing state", () => {
383
416
  beforeEach(() => {
384
417
  resetTables();
385
418
  mockFindMember = null;
386
419
  });
387
420
 
388
- function seedFailedEvent(trustClass: 'guardian' | 'trusted_contact' | 'unknown', guardianExternalUserId?: string): string {
389
- const inbound = channelDeliveryStore.recordInbound('telegram', `chat-${trustClass}`, `msg-${trustClass}-${Date.now()}`);
421
+ function seedFailedEvent(
422
+ trustClass: "guardian" | "trusted_contact" | "unknown",
423
+ guardianExternalUserId?: string,
424
+ ): string {
425
+ const inbound = channelDeliveryStore.recordInbound(
426
+ "telegram",
427
+ `chat-${trustClass}`,
428
+ `msg-${trustClass}-${Date.now()}`,
429
+ );
390
430
  channelDeliveryStore.storePayload(inbound.eventId, {
391
- content: 'retry me',
392
- sourceChannel: 'telegram',
393
- interface: 'telegram',
431
+ content: "retry me",
432
+ sourceChannel: "telegram",
433
+ interface: "telegram",
394
434
  guardianCtx: {
395
435
  trustClass,
396
- sourceChannel: 'telegram',
397
- requesterExternalUserId: 'test-user',
436
+ sourceChannel: "telegram",
437
+ requesterExternalUserId: "test-user",
398
438
  requesterChatId: `chat-${trustClass}`,
399
439
  ...(guardianExternalUserId ? { guardianExternalUserId } : {}),
400
440
  },
@@ -403,7 +443,7 @@ describe('channel-retry-sweep routing state', () => {
403
443
  const db = getDb();
404
444
  db.update(channelInboundEvents)
405
445
  .set({
406
- processingStatus: 'failed',
446
+ processingStatus: "failed",
407
447
  processingAttempts: 1,
408
448
  retryAfter: Date.now() - 1,
409
449
  })
@@ -413,8 +453,8 @@ describe('channel-retry-sweep routing state', () => {
413
453
  return inbound.eventId;
414
454
  }
415
455
 
416
- test('trusted_contact with guardian binding replays as interactive', async () => {
417
- seedFailedEvent('trusted_contact', 'guardian-for-sweep');
456
+ test("trusted_contact with guardian binding replays as interactive", async () => {
457
+ seedFailedEvent("trusted_contact", "guardian-for-sweep");
418
458
  let capturedOptions: { isInteractive?: boolean } | undefined;
419
459
 
420
460
  await sweepFailedEvents(
@@ -422,13 +462,15 @@ describe('channel-retry-sweep routing state', () => {
422
462
  capturedOptions = options as { isInteractive?: boolean };
423
463
  const messageId = `message-tc-sweep-${Date.now()}`;
424
464
  const db = getDb();
425
- db.insert(messages).values({
426
- id: messageId,
427
- conversationId,
428
- role: 'user',
429
- content: JSON.stringify([{ type: 'text', text: 'retry me' }]),
430
- createdAt: Date.now(),
431
- }).run();
465
+ db.insert(messages)
466
+ .values({
467
+ id: messageId,
468
+ conversationId,
469
+ role: "user",
470
+ content: JSON.stringify([{ type: "text", text: "retry me" }]),
471
+ createdAt: Date.now(),
472
+ })
473
+ .run();
432
474
  return { messageId };
433
475
  },
434
476
  undefined,
@@ -437,8 +479,8 @@ describe('channel-retry-sweep routing state', () => {
437
479
  expect(capturedOptions?.isInteractive).toBe(true);
438
480
  });
439
481
 
440
- test('trusted_contact without guardian binding replays as non-interactive', async () => {
441
- seedFailedEvent('trusted_contact');
482
+ test("trusted_contact without guardian binding replays as non-interactive", async () => {
483
+ seedFailedEvent("trusted_contact");
442
484
  let capturedOptions: { isInteractive?: boolean } | undefined;
443
485
 
444
486
  await sweepFailedEvents(
@@ -446,13 +488,15 @@ describe('channel-retry-sweep routing state', () => {
446
488
  capturedOptions = options as { isInteractive?: boolean };
447
489
  const messageId = `message-tc-no-binding-${Date.now()}`;
448
490
  const db = getDb();
449
- db.insert(messages).values({
450
- id: messageId,
451
- conversationId,
452
- role: 'user',
453
- content: JSON.stringify([{ type: 'text', text: 'retry me' }]),
454
- createdAt: Date.now(),
455
- }).run();
491
+ db.insert(messages)
492
+ .values({
493
+ id: messageId,
494
+ conversationId,
495
+ role: "user",
496
+ content: JSON.stringify([{ type: "text", text: "retry me" }]),
497
+ createdAt: Date.now(),
498
+ })
499
+ .run();
456
500
  return { messageId };
457
501
  },
458
502
  undefined,
@@ -461,8 +505,8 @@ describe('channel-retry-sweep routing state', () => {
461
505
  expect(capturedOptions?.isInteractive).toBe(false);
462
506
  });
463
507
 
464
- test('guardian replays as interactive', async () => {
465
- seedFailedEvent('guardian', 'guardian-self');
508
+ test("guardian replays as interactive", async () => {
509
+ seedFailedEvent("guardian", "guardian-self");
466
510
  let capturedOptions: { isInteractive?: boolean } | undefined;
467
511
 
468
512
  await sweepFailedEvents(
@@ -470,13 +514,15 @@ describe('channel-retry-sweep routing state', () => {
470
514
  capturedOptions = options as { isInteractive?: boolean };
471
515
  const messageId = `message-guardian-sweep-${Date.now()}`;
472
516
  const db = getDb();
473
- db.insert(messages).values({
474
- id: messageId,
475
- conversationId,
476
- role: 'user',
477
- content: JSON.stringify([{ type: 'text', text: 'retry me' }]),
478
- createdAt: Date.now(),
479
- }).run();
517
+ db.insert(messages)
518
+ .values({
519
+ id: messageId,
520
+ conversationId,
521
+ role: "user",
522
+ content: JSON.stringify([{ type: "text", text: "retry me" }]),
523
+ createdAt: Date.now(),
524
+ })
525
+ .run();
480
526
  return { messageId };
481
527
  },
482
528
  undefined,
@@ -485,8 +531,8 @@ describe('channel-retry-sweep routing state', () => {
485
531
  expect(capturedOptions?.isInteractive).toBe(true);
486
532
  });
487
533
 
488
- test('unknown replays as non-interactive', async () => {
489
- seedFailedEvent('unknown');
534
+ test("unknown replays as non-interactive", async () => {
535
+ seedFailedEvent("unknown");
490
536
  let capturedOptions: { isInteractive?: boolean } | undefined;
491
537
 
492
538
  await sweepFailedEvents(
@@ -494,13 +540,15 @@ describe('channel-retry-sweep routing state', () => {
494
540
  capturedOptions = options as { isInteractive?: boolean };
495
541
  const messageId = `message-unknown-sweep-${Date.now()}`;
496
542
  const db = getDb();
497
- db.insert(messages).values({
498
- id: messageId,
499
- conversationId,
500
- role: 'user',
501
- content: JSON.stringify([{ type: 'text', text: 'retry me' }]),
502
- createdAt: Date.now(),
503
- }).run();
543
+ db.insert(messages)
544
+ .values({
545
+ id: messageId,
546
+ conversationId,
547
+ role: "user",
548
+ content: JSON.stringify([{ type: "text", text: "retry me" }]),
549
+ createdAt: Date.now(),
550
+ })
551
+ .run();
504
552
  return { messageId };
505
553
  },
506
554
  undefined,