@vellumai/assistant 0.4.16 → 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 (58) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/package.json +1 -1
  4. package/src/__tests__/call-controller.test.ts +1074 -751
  5. package/src/__tests__/call-routes-http.test.ts +329 -279
  6. package/src/__tests__/channel-approval-routes.test.ts +0 -11
  7. package/src/__tests__/channel-approvals.test.ts +227 -182
  8. package/src/__tests__/channel-guardian.test.ts +1 -0
  9. package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
  10. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  11. package/src/__tests__/conversation-routes.test.ts +71 -41
  12. package/src/__tests__/daemon-server-session-init.test.ts +258 -191
  13. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
  14. package/src/__tests__/extract-email.test.ts +42 -0
  15. package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
  16. package/src/__tests__/gateway-only-guard.test.ts +54 -55
  17. package/src/__tests__/gmail-integration.test.ts +48 -46
  18. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
  19. package/src/__tests__/guardian-outbound-http.test.ts +334 -208
  20. package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
  21. package/src/__tests__/guardian-routing-state.test.ts +257 -209
  22. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
  23. package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
  24. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
  25. package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
  26. package/src/__tests__/ingress-reconcile.test.ts +184 -142
  27. package/src/__tests__/non-member-access-request.test.ts +291 -247
  28. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  29. package/src/__tests__/recording-intent-handler.test.ts +422 -291
  30. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
  31. package/src/__tests__/runtime-events-sse.test.ts +67 -50
  32. package/src/__tests__/send-endpoint-busy.test.ts +314 -232
  33. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  34. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  35. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
  36. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
  37. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
  38. package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
  39. package/src/__tests__/twilio-config.test.ts +49 -41
  40. package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
  41. package/src/__tests__/twilio-routes.test.ts +389 -280
  42. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
  43. package/src/config/bundled-skills/messaging/SKILL.md +5 -4
  44. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +11 -7
  45. package/src/config/env.ts +39 -29
  46. package/src/daemon/handlers/skills.ts +18 -10
  47. package/src/daemon/ipc-contract/messages.ts +1 -0
  48. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  49. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  50. package/src/daemon/session-agent-loop.ts +1 -1
  51. package/src/daemon/session-process.ts +1 -1
  52. package/src/daemon/session-surfaces.ts +42 -2
  53. package/src/runtime/auth/token-service.ts +74 -47
  54. package/src/sequence/reply-matcher.ts +10 -6
  55. package/src/skills/frontmatter.ts +9 -6
  56. package/src/tools/ui-surface/definitions.ts +2 -1
  57. package/src/util/platform.ts +0 -12
  58. package/docs/architecture/http-token-refresh.md +0 -274
@@ -5,52 +5,57 @@
5
5
  * POST /v1/calls/:id/cancel, and POST /v1/calls/:id/answer
6
6
  * through RuntimeHttpServer.
7
7
  */
8
- import { mkdtempSync, realpathSync,rmSync } from 'node:fs';
9
- import { tmpdir } from 'node:os';
10
- import { join } from 'node:path';
8
+ import { mkdtempSync, realpathSync, rmSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
11
 
12
- import { afterAll, beforeEach, describe, expect, mock,test } from 'bun:test';
12
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
13
13
 
14
- const testDir = realpathSync(mkdtempSync(join(tmpdir(), 'call-routes-http-test-')));
14
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
15
15
 
16
- mock.module('../util/platform.js', () => ({
16
+ const testDir = realpathSync(
17
+ mkdtempSync(join(tmpdir(), "call-routes-http-test-")),
18
+ );
19
+
20
+ mock.module("../util/platform.js", () => ({
17
21
  getRootDir: () => testDir,
18
22
  getDataDir: () => testDir,
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'),
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"),
26
30
  ensureDataDir: () => {},
27
31
  readHttpToken: () => null,
28
32
  }));
29
33
 
30
- mock.module('../util/logger.js', () => ({
31
- getLogger: () => new Proxy({} as Record<string, unknown>, {
32
- get: () => () => {},
33
- }),
34
+ mock.module("../util/logger.js", () => ({
35
+ getLogger: () =>
36
+ new Proxy({} as Record<string, unknown>, {
37
+ get: () => () => {},
38
+ }),
34
39
  }));
35
40
 
36
41
  const mockCallsConfig = {
37
42
  enabled: true,
38
- provider: 'twilio',
43
+ provider: "twilio",
39
44
  maxDurationSeconds: 3600,
40
45
  userConsultTimeoutSeconds: 120,
41
- disclosure: { enabled: false, text: '' },
46
+ disclosure: { enabled: false, text: "" },
42
47
  safety: { denyCategories: [] },
43
48
  callerIdentity: {
44
49
  allowPerCallOverride: true,
45
50
  },
46
51
  };
47
52
 
48
- mock.module('../config/loader.js', () => ({
53
+ mock.module("../config/loader.js", () => ({
49
54
  getConfig: () => ({
50
55
  ui: {},
51
-
52
- model: 'test',
53
- provider: 'test',
56
+
57
+ model: "test",
58
+ provider: "test",
54
59
  apiKeys: {},
55
60
  memory: { enabled: false },
56
61
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
@@ -58,8 +63,8 @@ mock.module('../config/loader.js', () => ({
58
63
  calls: mockCallsConfig,
59
64
  }),
60
65
  loadConfig: () => ({
61
- model: 'test',
62
- provider: 'test',
66
+ model: "test",
67
+ provider: "test",
63
68
  apiKeys: {},
64
69
  memory: { enabled: false },
65
70
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
@@ -67,34 +72,42 @@ mock.module('../config/loader.js', () => ({
67
72
  calls: mockCallsConfig,
68
73
  ingress: {
69
74
  enabled: true,
70
- publicBaseUrl: 'https://test.example.com',
75
+ publicBaseUrl: "https://test.example.com",
71
76
  },
72
77
  }),
73
78
  }));
74
79
 
75
80
  // Mock Twilio provider to avoid real API calls
76
- mock.module('../calls/twilio-provider.js', () => ({
81
+ mock.module("../calls/twilio-provider.js", () => ({
77
82
  TwilioConversationRelayProvider: class {
78
- static getAuthToken() { return 'mock-auth-token'; }
79
- static verifyWebhookSignature() { return true; }
80
- async initiateCall() { return { callSid: 'CA_mock_sid_123' }; }
81
- async endCall() { return; }
83
+ static getAuthToken() {
84
+ return "mock-auth-token";
85
+ }
86
+ static verifyWebhookSignature() {
87
+ return true;
88
+ }
89
+ async initiateCall() {
90
+ return { callSid: "CA_mock_sid_123" };
91
+ }
92
+ async endCall() {
93
+ return;
94
+ }
82
95
  },
83
96
  }));
84
97
 
85
98
  // Mock Twilio config
86
- mock.module('../calls/twilio-config.js', () => ({
99
+ mock.module("../calls/twilio-config.js", () => ({
87
100
  getTwilioConfig: (assistantId?: string) => ({
88
- accountSid: 'AC_test',
89
- authToken: 'test_token',
90
- phoneNumber: assistantId === 'asst-alpha' ? '+15550009999' : '+15550001111',
91
- webhookBaseUrl: 'https://test.example.com',
92
- wssBaseUrl: 'wss://test.example.com',
101
+ accountSid: "AC_test",
102
+ authToken: "test_token",
103
+ phoneNumber: assistantId === "asst-alpha" ? "+15550009999" : "+15550001111",
104
+ webhookBaseUrl: "https://test.example.com",
105
+ wssBaseUrl: "wss://test.example.com",
93
106
  }),
94
107
  }));
95
108
 
96
109
  // Mock secure keys
97
- mock.module('../security/secure-keys.js', () => ({
110
+ mock.module("../security/secure-keys.js", () => ({
98
111
  getSecureKey: () => null,
99
112
  }));
100
113
 
@@ -102,12 +115,12 @@ import {
102
115
  createCallSession,
103
116
  createPendingQuestion,
104
117
  updateCallSession,
105
- } from '../calls/call-store.js';
106
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
107
- import { conversations } from '../memory/schema.js';
108
- import { RuntimeHttpServer } from '../runtime/http-server.js';
118
+ } from "../calls/call-store.js";
119
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
120
+ import { conversations } from "../memory/schema.js";
121
+ import { RuntimeHttpServer } from "../runtime/http-server.js";
109
122
 
110
- import '../calls/call-state.js';
123
+ import "../calls/call-state.js";
111
124
 
112
125
  initializeDb();
113
126
 
@@ -115,7 +128,7 @@ initializeDb();
115
128
  // Helpers
116
129
  // ---------------------------------------------------------------------------
117
130
 
118
- const TEST_TOKEN = 'test-bearer-token-calls';
131
+ const TEST_TOKEN = "test-bearer-token-calls";
119
132
  const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
120
133
 
121
134
  let ensuredConvIds = new Set<string>();
@@ -124,25 +137,27 @@ function ensureConversation(id: string): void {
124
137
  if (ensuredConvIds.has(id)) return;
125
138
  const db = getDb();
126
139
  const now = Date.now();
127
- db.insert(conversations).values({
128
- id,
129
- title: `Test conversation ${id}`,
130
- createdAt: now,
131
- updatedAt: now,
132
- }).run();
140
+ db.insert(conversations)
141
+ .values({
142
+ id,
143
+ title: `Test conversation ${id}`,
144
+ createdAt: now,
145
+ updatedAt: now,
146
+ })
147
+ .run();
133
148
  ensuredConvIds.add(id);
134
149
  }
135
150
 
136
151
  function resetTables() {
137
152
  const db = getDb();
138
- db.run('DELETE FROM guardian_action_deliveries');
139
- db.run('DELETE FROM guardian_action_requests');
140
- db.run('DELETE FROM call_pending_questions');
141
- db.run('DELETE FROM call_events');
142
- db.run('DELETE FROM call_sessions');
143
- db.run('DELETE FROM tool_invocations');
144
- db.run('DELETE FROM messages');
145
- db.run('DELETE FROM conversations');
153
+ db.run("DELETE FROM guardian_action_deliveries");
154
+ db.run("DELETE FROM guardian_action_requests");
155
+ db.run("DELETE FROM call_pending_questions");
156
+ db.run("DELETE FROM call_events");
157
+ db.run("DELETE FROM call_sessions");
158
+ db.run("DELETE FROM tool_invocations");
159
+ db.run("DELETE FROM messages");
160
+ db.run("DELETE FROM conversations");
146
161
  ensuredConvIds = new Set();
147
162
  }
148
163
 
@@ -150,7 +165,7 @@ function resetTables() {
150
165
  // Tests
151
166
  // ---------------------------------------------------------------------------
152
167
 
153
- describe('runtime call routes — HTTP layer', () => {
168
+ describe("runtime call routes — HTTP layer", () => {
154
169
  let server: RuntimeHttpServer;
155
170
  let port: number;
156
171
 
@@ -160,7 +175,11 @@ describe('runtime call routes — HTTP layer', () => {
160
175
 
161
176
  afterAll(() => {
162
177
  resetDb();
163
- try { rmSync(testDir, { recursive: true, force: true }); } catch { /* best effort */ }
178
+ try {
179
+ rmSync(testDir, { recursive: true, force: true });
180
+ } catch {
181
+ /* best effort */
182
+ }
164
183
  });
165
184
 
166
185
  async function startServer(): Promise<void> {
@@ -173,29 +192,29 @@ describe('runtime call routes — HTTP layer', () => {
173
192
  await server?.stop();
174
193
  }
175
194
 
176
- function callsUrl(path = ''): string {
195
+ function callsUrl(path = ""): string {
177
196
  return `http://127.0.0.1:${port}/v1/calls${path}`;
178
197
  }
179
198
 
180
199
  // ── POST /v1/calls/start ────────────────────────────────────────────
181
200
 
182
- test('POST /v1/calls/start returns 201 with call session', async () => {
201
+ test("POST /v1/calls/start returns 201 with call session", async () => {
183
202
  await startServer();
184
- ensureConversation('conv-start-1');
203
+ ensureConversation("conv-start-1");
185
204
 
186
- const res = await fetch(callsUrl('/start'), {
187
- method: 'POST',
188
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
205
+ const res = await fetch(callsUrl("/start"), {
206
+ method: "POST",
207
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
189
208
  body: JSON.stringify({
190
- phoneNumber: '+15559998888',
191
- task: 'Book a table for two',
192
- conversationId: 'conv-start-1',
209
+ phoneNumber: "+15559998888",
210
+ task: "Book a table for two",
211
+ conversationId: "conv-start-1",
193
212
  }),
194
213
  });
195
214
 
196
215
  expect(res.status).toBe(201);
197
216
 
198
- const body = await res.json() as {
217
+ const body = (await res.json()) as {
199
218
  callSessionId: string;
200
219
  callSid: string;
201
220
  status: string;
@@ -204,111 +223,119 @@ describe('runtime call routes — HTTP layer', () => {
204
223
  };
205
224
 
206
225
  expect(body.callSessionId).toBeDefined();
207
- expect(body.callSid).toBe('CA_mock_sid_123');
208
- expect(body.status).toBe('initiated');
209
- expect(body.toNumber).toBe('+15559998888');
210
- expect(body.fromNumber).toBe('+15550001111');
226
+ expect(body.callSid).toBe("CA_mock_sid_123");
227
+ expect(body.status).toBe("initiated");
228
+ expect(body.toNumber).toBe("+15559998888");
229
+ expect(body.fromNumber).toBe("+15550001111");
211
230
 
212
231
  await stopServer();
213
232
  });
214
233
 
215
- test('POST /v1/calls/start returns 400 when conversationId missing', async () => {
234
+ test("POST /v1/calls/start returns 400 when conversationId missing", async () => {
216
235
  await startServer();
217
236
 
218
- const res = await fetch(callsUrl('/start'), {
219
- method: 'POST',
220
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
237
+ const res = await fetch(callsUrl("/start"), {
238
+ method: "POST",
239
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
221
240
  body: JSON.stringify({
222
- phoneNumber: '+15559998888',
223
- task: 'Book a table',
241
+ phoneNumber: "+15559998888",
242
+ task: "Book a table",
224
243
  }),
225
244
  });
226
245
 
227
246
  expect(res.status).toBe(400);
228
- const body = await res.json() as { error: { message: string; code?: string } };
229
- expect(body.error.message).toContain('conversationId');
247
+ const body = (await res.json()) as {
248
+ error: { message: string; code?: string };
249
+ };
250
+ expect(body.error.message).toContain("conversationId");
230
251
 
231
252
  await stopServer();
232
253
  });
233
254
 
234
- test('POST /v1/calls/start returns 400 for invalid phone number', async () => {
255
+ test("POST /v1/calls/start returns 400 for invalid phone number", async () => {
235
256
  await startServer();
236
- ensureConversation('conv-start-2');
257
+ ensureConversation("conv-start-2");
237
258
 
238
- const res = await fetch(callsUrl('/start'), {
239
- method: 'POST',
240
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
259
+ const res = await fetch(callsUrl("/start"), {
260
+ method: "POST",
261
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
241
262
  body: JSON.stringify({
242
- phoneNumber: 'not-a-number',
243
- task: 'Book a table',
244
- conversationId: 'conv-start-2',
263
+ phoneNumber: "not-a-number",
264
+ task: "Book a table",
265
+ conversationId: "conv-start-2",
245
266
  }),
246
267
  });
247
268
 
248
269
  expect(res.status).toBe(400);
249
- const body = await res.json() as { error: { message: string; code?: string } };
250
- expect(body.error.message).toContain('E.164');
270
+ const body = (await res.json()) as {
271
+ error: { message: string; code?: string };
272
+ };
273
+ expect(body.error.message).toContain("E.164");
251
274
 
252
275
  await stopServer();
253
276
  });
254
277
 
255
- test('POST /v1/calls/start returns 400 for malformed JSON', async () => {
278
+ test("POST /v1/calls/start returns 400 for malformed JSON", async () => {
256
279
  await startServer();
257
280
 
258
- const res = await fetch(callsUrl('/start'), {
259
- method: 'POST',
260
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
261
- body: 'not-json{{',
281
+ const res = await fetch(callsUrl("/start"), {
282
+ method: "POST",
283
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
284
+ body: "not-json{{",
262
285
  });
263
286
 
264
287
  expect(res.status).toBe(400);
265
- const body = await res.json() as { error: { message: string; code?: string } };
266
- expect(body.error.message).toContain('Invalid JSON');
288
+ const body = (await res.json()) as {
289
+ error: { message: string; code?: string };
290
+ };
291
+ expect(body.error.message).toContain("Invalid JSON");
267
292
 
268
293
  await stopServer();
269
294
  });
270
295
 
271
- test('POST /v1/calls/start with callerIdentityMode user_number is accepted', async () => {
296
+ test("POST /v1/calls/start with callerIdentityMode user_number is accepted", async () => {
272
297
  await startServer();
273
- ensureConversation('conv-start-identity-1');
298
+ ensureConversation("conv-start-identity-1");
274
299
 
275
- const res = await fetch(callsUrl('/start'), {
276
- method: 'POST',
277
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
300
+ const res = await fetch(callsUrl("/start"), {
301
+ method: "POST",
302
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
278
303
  body: JSON.stringify({
279
- phoneNumber: '+15559998888',
280
- task: 'Book a table for two',
281
- conversationId: 'conv-start-identity-1',
282
- callerIdentityMode: 'user_number',
304
+ phoneNumber: "+15559998888",
305
+ task: "Book a table for two",
306
+ conversationId: "conv-start-identity-1",
307
+ callerIdentityMode: "user_number",
283
308
  }),
284
309
  });
285
310
 
286
311
  // user_number mode requires a configured user phone number;
287
312
  // since we haven't set one, this should return a 400 explaining why
288
313
  expect(res.status).toBe(400);
289
- const body = await res.json() as { error: { message: string; code?: string } };
290
- expect(body.error.message).toContain('user_number');
314
+ const body = (await res.json()) as {
315
+ error: { message: string; code?: string };
316
+ };
317
+ expect(body.error.message).toContain("user_number");
291
318
 
292
319
  await stopServer();
293
320
  });
294
321
 
295
- test('POST /v1/calls/start without callerIdentityMode defaults to assistant_number', async () => {
322
+ test("POST /v1/calls/start without callerIdentityMode defaults to assistant_number", async () => {
296
323
  await startServer();
297
- ensureConversation('conv-start-identity-2');
324
+ ensureConversation("conv-start-identity-2");
298
325
 
299
- const res = await fetch(callsUrl('/start'), {
300
- method: 'POST',
301
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
326
+ const res = await fetch(callsUrl("/start"), {
327
+ method: "POST",
328
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
302
329
  body: JSON.stringify({
303
- phoneNumber: '+15559998888',
304
- task: 'Book a table for two',
305
- conversationId: 'conv-start-identity-2',
330
+ phoneNumber: "+15559998888",
331
+ task: "Book a table for two",
332
+ conversationId: "conv-start-identity-2",
306
333
  }),
307
334
  });
308
335
 
309
336
  expect(res.status).toBe(201);
310
337
 
311
- const body = await res.json() as {
338
+ const body = (await res.json()) as {
312
339
  callSessionId: string;
313
340
  callSid: string;
314
341
  status: string;
@@ -318,50 +345,52 @@ describe('runtime call routes — HTTP layer', () => {
318
345
  };
319
346
 
320
347
  expect(body.callSessionId).toBeDefined();
321
- expect(body.callSid).toBe('CA_mock_sid_123');
322
- expect(body.fromNumber).toBe('+15550001111');
323
- expect(body.callerIdentityMode).toBe('assistant_number');
348
+ expect(body.callSid).toBe("CA_mock_sid_123");
349
+ expect(body.fromNumber).toBe("+15550001111");
350
+ expect(body.callerIdentityMode).toBe("assistant_number");
324
351
 
325
352
  await stopServer();
326
353
  });
327
354
 
328
- test('POST /v1/calls/start returns 400 for invalid callerIdentityMode', async () => {
355
+ test("POST /v1/calls/start returns 400 for invalid callerIdentityMode", async () => {
329
356
  await startServer();
330
- ensureConversation('conv-start-identity-bogus');
357
+ ensureConversation("conv-start-identity-bogus");
331
358
 
332
- const res = await fetch(callsUrl('/start'), {
333
- method: 'POST',
334
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
359
+ const res = await fetch(callsUrl("/start"), {
360
+ method: "POST",
361
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
335
362
  body: JSON.stringify({
336
- phoneNumber: '+15559998888',
337
- task: 'Book a table for two',
338
- conversationId: 'conv-start-identity-bogus',
339
- callerIdentityMode: 'bogus',
363
+ phoneNumber: "+15559998888",
364
+ task: "Book a table for two",
365
+ conversationId: "conv-start-identity-bogus",
366
+ callerIdentityMode: "bogus",
340
367
  }),
341
368
  });
342
369
 
343
370
  expect(res.status).toBe(400);
344
- const body = await res.json() as { error: { message: string; code?: string } };
345
- expect(body.error.message).toContain('Invalid callerIdentityMode');
346
- expect(body.error.message).toContain('bogus');
347
- expect(body.error.message).toContain('assistant_number');
348
- expect(body.error.message).toContain('user_number');
371
+ const body = (await res.json()) as {
372
+ error: { message: string; code?: string };
373
+ };
374
+ expect(body.error.message).toContain("Invalid callerIdentityMode");
375
+ expect(body.error.message).toContain("bogus");
376
+ expect(body.error.message).toContain("assistant_number");
377
+ expect(body.error.message).toContain("user_number");
349
378
 
350
379
  await stopServer();
351
380
  });
352
381
 
353
382
  // ── GET /v1/calls/:id ───────────────────────────────────────────────
354
383
 
355
- test('GET /v1/calls/:id returns call status', async () => {
384
+ test("GET /v1/calls/:id returns call status", async () => {
356
385
  await startServer();
357
- ensureConversation('conv-get-1');
386
+ ensureConversation("conv-get-1");
358
387
 
359
388
  const session = createCallSession({
360
- conversationId: 'conv-get-1',
361
- provider: 'twilio',
362
- fromNumber: '+15550001111',
363
- toNumber: '+15559998888',
364
- task: 'Test task',
389
+ conversationId: "conv-get-1",
390
+ provider: "twilio",
391
+ fromNumber: "+15550001111",
392
+ toNumber: "+15559998888",
393
+ task: "Test task",
365
394
  });
366
395
 
367
396
  const res = await fetch(callsUrl(`/${session.id}`), {
@@ -370,7 +399,7 @@ describe('runtime call routes — HTTP layer', () => {
370
399
 
371
400
  expect(res.status).toBe(200);
372
401
 
373
- const body = await res.json() as {
402
+ const body = (await res.json()) as {
374
403
  callSessionId: string;
375
404
  status: string;
376
405
  toNumber: string;
@@ -380,19 +409,19 @@ describe('runtime call routes — HTTP layer', () => {
380
409
  };
381
410
 
382
411
  expect(body.callSessionId).toBe(session.id);
383
- expect(body.status).toBe('initiated');
384
- expect(body.toNumber).toBe('+15559998888');
385
- expect(body.fromNumber).toBe('+15550001111');
386
- expect(body.task).toBe('Test task');
412
+ expect(body.status).toBe("initiated");
413
+ expect(body.toNumber).toBe("+15559998888");
414
+ expect(body.fromNumber).toBe("+15550001111");
415
+ expect(body.task).toBe("Test task");
387
416
  expect(body.pendingQuestion).toBeNull();
388
417
 
389
418
  await stopServer();
390
419
  });
391
420
 
392
- test('GET /v1/calls/:id returns 404 for unknown session', async () => {
421
+ test("GET /v1/calls/:id returns 404 for unknown session", async () => {
393
422
  await startServer();
394
423
 
395
- const res = await fetch(callsUrl('/nonexistent-id'), {
424
+ const res = await fetch(callsUrl("/nonexistent-id"), {
396
425
  headers: AUTH_HEADERS,
397
426
  });
398
427
 
@@ -403,48 +432,51 @@ describe('runtime call routes — HTTP layer', () => {
403
432
 
404
433
  // ── POST /v1/calls/:id/cancel ──────────────────────────────────────
405
434
 
406
- test('POST /v1/calls/:id/cancel transitions to cancelled', async () => {
435
+ test("POST /v1/calls/:id/cancel transitions to cancelled", async () => {
407
436
  await startServer();
408
- ensureConversation('conv-cancel-1');
437
+ ensureConversation("conv-cancel-1");
409
438
 
410
439
  const session = createCallSession({
411
- conversationId: 'conv-cancel-1',
412
- provider: 'twilio',
413
- fromNumber: '+15550001111',
414
- toNumber: '+15559998888',
440
+ conversationId: "conv-cancel-1",
441
+ provider: "twilio",
442
+ fromNumber: "+15550001111",
443
+ toNumber: "+15559998888",
415
444
  });
416
445
 
417
446
  const res = await fetch(callsUrl(`/${session.id}/cancel`), {
418
- method: 'POST',
419
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
420
- body: JSON.stringify({ reason: 'User requested' }),
447
+ method: "POST",
448
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
449
+ body: JSON.stringify({ reason: "User requested" }),
421
450
  });
422
451
 
423
452
  expect(res.status).toBe(200);
424
453
 
425
- const body = await res.json() as { callSessionId: string; status: string };
454
+ const body = (await res.json()) as {
455
+ callSessionId: string;
456
+ status: string;
457
+ };
426
458
  expect(body.callSessionId).toBe(session.id);
427
- expect(body.status).toBe('cancelled');
459
+ expect(body.status).toBe("cancelled");
428
460
 
429
461
  await stopServer();
430
462
  });
431
463
 
432
- test('POST /v1/calls/:id/cancel returns 409 for already-ended call', async () => {
464
+ test("POST /v1/calls/:id/cancel returns 409 for already-ended call", async () => {
433
465
  await startServer();
434
- ensureConversation('conv-cancel-2');
466
+ ensureConversation("conv-cancel-2");
435
467
 
436
468
  const session = createCallSession({
437
- conversationId: 'conv-cancel-2',
438
- provider: 'twilio',
439
- fromNumber: '+15550001111',
440
- toNumber: '+15559998888',
469
+ conversationId: "conv-cancel-2",
470
+ provider: "twilio",
471
+ fromNumber: "+15550001111",
472
+ toNumber: "+15559998888",
441
473
  });
442
474
 
443
- updateCallSession(session.id, { status: 'completed', endedAt: Date.now() });
475
+ updateCallSession(session.id, { status: "completed", endedAt: Date.now() });
444
476
 
445
477
  const res = await fetch(callsUrl(`/${session.id}/cancel`), {
446
- method: 'POST',
447
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
478
+ method: "POST",
479
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
448
480
  body: JSON.stringify({}),
449
481
  });
450
482
 
@@ -453,12 +485,12 @@ describe('runtime call routes — HTTP layer', () => {
453
485
  await stopServer();
454
486
  });
455
487
 
456
- test('POST /v1/calls/:id/cancel returns 404 for unknown session', async () => {
488
+ test("POST /v1/calls/:id/cancel returns 404 for unknown session", async () => {
457
489
  await startServer();
458
490
 
459
- const res = await fetch(callsUrl('/nonexistent-id/cancel'), {
460
- method: 'POST',
461
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
491
+ const res = await fetch(callsUrl("/nonexistent-id/cancel"), {
492
+ method: "POST",
493
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
462
494
  body: JSON.stringify({}),
463
495
  });
464
496
 
@@ -469,69 +501,73 @@ describe('runtime call routes — HTTP layer', () => {
469
501
 
470
502
  // ── POST /v1/calls/:id/answer ──────────────────────────────────────
471
503
 
472
- test('POST /v1/calls/:id/answer returns 400 for malformed JSON', async () => {
504
+ test("POST /v1/calls/:id/answer returns 400 for malformed JSON", async () => {
473
505
  await startServer();
474
- ensureConversation('conv-answer-badjson');
506
+ ensureConversation("conv-answer-badjson");
475
507
 
476
508
  const session = createCallSession({
477
- conversationId: 'conv-answer-badjson',
478
- provider: 'twilio',
479
- fromNumber: '+15550001111',
480
- toNumber: '+15559998888',
509
+ conversationId: "conv-answer-badjson",
510
+ provider: "twilio",
511
+ fromNumber: "+15550001111",
512
+ toNumber: "+15559998888",
481
513
  });
482
514
 
483
515
  const res = await fetch(callsUrl(`/${session.id}/answer`), {
484
- method: 'POST',
485
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
486
- body: 'not-json{{',
516
+ method: "POST",
517
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
518
+ body: "not-json{{",
487
519
  });
488
520
 
489
521
  expect(res.status).toBe(400);
490
- const body = await res.json() as { error: { message: string; code?: string } };
491
- expect(body.error.message).toContain('Invalid JSON');
522
+ const body = (await res.json()) as {
523
+ error: { message: string; code?: string };
524
+ };
525
+ expect(body.error.message).toContain("Invalid JSON");
492
526
 
493
527
  await stopServer();
494
528
  });
495
529
 
496
- test('POST /v1/calls/:id/answer returns 404 when no pending question', async () => {
530
+ test("POST /v1/calls/:id/answer returns 404 when no pending question", async () => {
497
531
  await startServer();
498
- ensureConversation('conv-answer-1');
532
+ ensureConversation("conv-answer-1");
499
533
 
500
534
  const session = createCallSession({
501
- conversationId: 'conv-answer-1',
502
- provider: 'twilio',
503
- fromNumber: '+15550001111',
504
- toNumber: '+15559998888',
535
+ conversationId: "conv-answer-1",
536
+ provider: "twilio",
537
+ fromNumber: "+15550001111",
538
+ toNumber: "+15559998888",
505
539
  });
506
540
 
507
541
  const res = await fetch(callsUrl(`/${session.id}/answer`), {
508
- method: 'POST',
509
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
510
- body: JSON.stringify({ answer: 'Yes, please' }),
542
+ method: "POST",
543
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
544
+ body: JSON.stringify({ answer: "Yes, please" }),
511
545
  });
512
546
 
513
547
  expect(res.status).toBe(409);
514
- const body = await res.json() as { error: { message: string; code?: string } };
515
- expect(body.error.message).toContain('No active controller');
548
+ const body = (await res.json()) as {
549
+ error: { message: string; code?: string };
550
+ };
551
+ expect(body.error.message).toContain("No active controller");
516
552
 
517
553
  await stopServer();
518
554
  });
519
555
 
520
- test('POST /v1/calls/:id/answer returns 400 when answer is empty', async () => {
556
+ test("POST /v1/calls/:id/answer returns 400 when answer is empty", async () => {
521
557
  await startServer();
522
- ensureConversation('conv-answer-2');
558
+ ensureConversation("conv-answer-2");
523
559
 
524
560
  const session = createCallSession({
525
- conversationId: 'conv-answer-2',
526
- provider: 'twilio',
527
- fromNumber: '+15550001111',
528
- toNumber: '+15559998888',
561
+ conversationId: "conv-answer-2",
562
+ provider: "twilio",
563
+ fromNumber: "+15550001111",
564
+ toNumber: "+15559998888",
529
565
  });
530
566
 
531
567
  const res = await fetch(callsUrl(`/${session.id}/answer`), {
532
- method: 'POST',
533
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
534
- body: JSON.stringify({ answer: '' }),
568
+ method: "POST",
569
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
570
+ body: JSON.stringify({ answer: "" }),
535
571
  });
536
572
 
537
573
  expect(res.status).toBe(400);
@@ -539,169 +575,183 @@ describe('runtime call routes — HTTP layer', () => {
539
575
  await stopServer();
540
576
  });
541
577
 
542
- test('POST /v1/calls/:id/answer returns 409 when no orchestrator', async () => {
578
+ test("POST /v1/calls/:id/answer returns 409 when no orchestrator", async () => {
543
579
  await startServer();
544
- ensureConversation('conv-answer-3');
580
+ ensureConversation("conv-answer-3");
545
581
 
546
582
  const session = createCallSession({
547
- conversationId: 'conv-answer-3',
548
- provider: 'twilio',
549
- fromNumber: '+15550001111',
550
- toNumber: '+15559998888',
583
+ conversationId: "conv-answer-3",
584
+ provider: "twilio",
585
+ fromNumber: "+15550001111",
586
+ toNumber: "+15559998888",
551
587
  });
552
588
 
553
589
  // Create a pending question but no orchestrator
554
- createPendingQuestion(session.id, 'What date do you prefer?');
590
+ createPendingQuestion(session.id, "What date do you prefer?");
555
591
 
556
592
  const res = await fetch(callsUrl(`/${session.id}/answer`), {
557
- method: 'POST',
558
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
559
- body: JSON.stringify({ answer: 'Tomorrow' }),
593
+ method: "POST",
594
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
595
+ body: JSON.stringify({ answer: "Tomorrow" }),
560
596
  });
561
597
 
562
598
  expect(res.status).toBe(409);
563
- const body = await res.json() as { error: { message: string; code?: string } };
564
- expect(body.error.message).toContain('No active controller');
599
+ const body = (await res.json()) as {
600
+ error: { message: string; code?: string };
601
+ };
602
+ expect(body.error.message).toContain("No active controller");
565
603
 
566
604
  await stopServer();
567
605
  });
568
606
 
569
607
  // ── POST /v1/calls/:id/instruction ────────────────────────────────
570
608
 
571
- test('POST /v1/calls/:id/instruction returns 400 for malformed JSON', async () => {
609
+ test("POST /v1/calls/:id/instruction returns 400 for malformed JSON", async () => {
572
610
  await startServer();
573
- ensureConversation('conv-instr-badjson');
611
+ ensureConversation("conv-instr-badjson");
574
612
 
575
613
  const session = createCallSession({
576
- conversationId: 'conv-instr-badjson',
577
- provider: 'twilio',
578
- fromNumber: '+15550001111',
579
- toNumber: '+15559998888',
614
+ conversationId: "conv-instr-badjson",
615
+ provider: "twilio",
616
+ fromNumber: "+15550001111",
617
+ toNumber: "+15559998888",
580
618
  });
581
619
 
582
620
  const res = await fetch(callsUrl(`/${session.id}/instruction`), {
583
- method: 'POST',
584
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
585
- body: 'not-json{{',
621
+ method: "POST",
622
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
623
+ body: "not-json{{",
586
624
  });
587
625
 
588
626
  expect(res.status).toBe(400);
589
- const body = await res.json() as { error: { message: string; code?: string } };
590
- expect(body.error.message).toContain('Invalid JSON');
627
+ const body = (await res.json()) as {
628
+ error: { message: string; code?: string };
629
+ };
630
+ expect(body.error.message).toContain("Invalid JSON");
591
631
 
592
632
  await stopServer();
593
633
  });
594
634
 
595
- test('POST /v1/calls/:id/instruction returns 400 when instruction is empty', async () => {
635
+ test("POST /v1/calls/:id/instruction returns 400 when instruction is empty", async () => {
596
636
  await startServer();
597
- ensureConversation('conv-instr-empty');
637
+ ensureConversation("conv-instr-empty");
598
638
 
599
639
  const session = createCallSession({
600
- conversationId: 'conv-instr-empty',
601
- provider: 'twilio',
602
- fromNumber: '+15550001111',
603
- toNumber: '+15559998888',
640
+ conversationId: "conv-instr-empty",
641
+ provider: "twilio",
642
+ fromNumber: "+15550001111",
643
+ toNumber: "+15559998888",
604
644
  });
605
645
 
606
646
  const res = await fetch(callsUrl(`/${session.id}/instruction`), {
607
- method: 'POST',
608
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
609
- body: JSON.stringify({ instruction: '' }),
647
+ method: "POST",
648
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
649
+ body: JSON.stringify({ instruction: "" }),
610
650
  });
611
651
 
612
652
  expect(res.status).toBe(400);
613
- const body = await res.json() as { error: { message: string; code?: string } };
614
- expect(body.error.message).toContain('instructionText');
653
+ const body = (await res.json()) as {
654
+ error: { message: string; code?: string };
655
+ };
656
+ expect(body.error.message).toContain("instructionText");
615
657
 
616
658
  await stopServer();
617
659
  });
618
660
 
619
- test('POST /v1/calls/:id/instruction returns 400 when instruction field is missing', async () => {
661
+ test("POST /v1/calls/:id/instruction returns 400 when instruction field is missing", async () => {
620
662
  await startServer();
621
- ensureConversation('conv-instr-missing');
663
+ ensureConversation("conv-instr-missing");
622
664
 
623
665
  const session = createCallSession({
624
- conversationId: 'conv-instr-missing',
625
- provider: 'twilio',
626
- fromNumber: '+15550001111',
627
- toNumber: '+15559998888',
666
+ conversationId: "conv-instr-missing",
667
+ provider: "twilio",
668
+ fromNumber: "+15550001111",
669
+ toNumber: "+15559998888",
628
670
  });
629
671
 
630
672
  const res = await fetch(callsUrl(`/${session.id}/instruction`), {
631
- method: 'POST',
632
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
673
+ method: "POST",
674
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
633
675
  body: JSON.stringify({}),
634
676
  });
635
677
 
636
678
  expect(res.status).toBe(400);
637
- const body = await res.json() as { error: { message: string; code?: string } };
638
- expect(body.error.message).toContain('instructionText');
679
+ const body = (await res.json()) as {
680
+ error: { message: string; code?: string };
681
+ };
682
+ expect(body.error.message).toContain("instructionText");
639
683
 
640
684
  await stopServer();
641
685
  });
642
686
 
643
- test('POST /v1/calls/:id/instruction returns 404 for unknown session', async () => {
687
+ test("POST /v1/calls/:id/instruction returns 404 for unknown session", async () => {
644
688
  await startServer();
645
689
 
646
- const res = await fetch(callsUrl('/nonexistent-id/instruction'), {
647
- method: 'POST',
648
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
649
- body: JSON.stringify({ instruction: 'Speed things up' }),
690
+ const res = await fetch(callsUrl("/nonexistent-id/instruction"), {
691
+ method: "POST",
692
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
693
+ body: JSON.stringify({ instruction: "Speed things up" }),
650
694
  });
651
695
 
652
696
  expect(res.status).toBe(404);
653
- const body = await res.json() as { error: { message: string; code?: string } };
654
- expect(body.error.message).toContain('No call session found');
697
+ const body = (await res.json()) as {
698
+ error: { message: string; code?: string };
699
+ };
700
+ expect(body.error.message).toContain("No call session found");
655
701
 
656
702
  await stopServer();
657
703
  });
658
704
 
659
- test('POST /v1/calls/:id/instruction returns 409 for ended call', async () => {
705
+ test("POST /v1/calls/:id/instruction returns 409 for ended call", async () => {
660
706
  await startServer();
661
- ensureConversation('conv-instr-ended');
707
+ ensureConversation("conv-instr-ended");
662
708
 
663
709
  const session = createCallSession({
664
- conversationId: 'conv-instr-ended',
665
- provider: 'twilio',
666
- fromNumber: '+15550001111',
667
- toNumber: '+15559998888',
710
+ conversationId: "conv-instr-ended",
711
+ provider: "twilio",
712
+ fromNumber: "+15550001111",
713
+ toNumber: "+15559998888",
668
714
  });
669
715
 
670
- updateCallSession(session.id, { status: 'completed', endedAt: Date.now() });
716
+ updateCallSession(session.id, { status: "completed", endedAt: Date.now() });
671
717
 
672
718
  const res = await fetch(callsUrl(`/${session.id}/instruction`), {
673
- method: 'POST',
674
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
675
- body: JSON.stringify({ instruction: 'Speed things up' }),
719
+ method: "POST",
720
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
721
+ body: JSON.stringify({ instruction: "Speed things up" }),
676
722
  });
677
723
 
678
724
  expect(res.status).toBe(409);
679
- const body = await res.json() as { error: { message: string; code?: string } };
680
- expect(body.error.message).toContain('not active');
725
+ const body = (await res.json()) as {
726
+ error: { message: string; code?: string };
727
+ };
728
+ expect(body.error.message).toContain("not active");
681
729
 
682
730
  await stopServer();
683
731
  });
684
732
 
685
- test('POST /v1/calls/:id/instruction returns 409 when no orchestrator', async () => {
733
+ test("POST /v1/calls/:id/instruction returns 409 when no orchestrator", async () => {
686
734
  await startServer();
687
- ensureConversation('conv-instr-no-orch');
735
+ ensureConversation("conv-instr-no-orch");
688
736
 
689
737
  const session = createCallSession({
690
- conversationId: 'conv-instr-no-orch',
691
- provider: 'twilio',
692
- fromNumber: '+15550001111',
693
- toNumber: '+15559998888',
738
+ conversationId: "conv-instr-no-orch",
739
+ provider: "twilio",
740
+ fromNumber: "+15550001111",
741
+ toNumber: "+15559998888",
694
742
  });
695
743
 
696
744
  const res = await fetch(callsUrl(`/${session.id}/instruction`), {
697
- method: 'POST',
698
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
699
- body: JSON.stringify({ instruction: 'Speed things up' }),
745
+ method: "POST",
746
+ headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
747
+ body: JSON.stringify({ instruction: "Speed things up" }),
700
748
  });
701
749
 
702
750
  expect(res.status).toBe(409);
703
- const body = await res.json() as { error: { message: string; code?: string } };
704
- expect(body.error.message).toContain('No active controller');
751
+ const body = (await res.json()) as {
752
+ error: { message: string; code?: string };
753
+ };
754
+ expect(body.error.message).toContain("No active controller");
705
755
 
706
756
  await stopServer();
707
757
  });