@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
@@ -1,27 +1,37 @@
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, type Mock,mock, test } from 'bun:test';
5
+ import {
6
+ afterAll,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ type Mock,
11
+ mock,
12
+ test,
13
+ } from "bun:test";
14
+
15
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
6
16
 
7
- const testDir = mkdtempSync(join(tmpdir(), 'call-controller-test-'));
17
+ const testDir = mkdtempSync(join(tmpdir(), "call-controller-test-"));
8
18
 
9
19
  // ── Platform + logger mocks (must come before any source imports) ────
10
20
 
11
- mock.module('../util/platform.js', () => ({
21
+ mock.module("../util/platform.js", () => ({
12
22
  getDataDir: () => testDir,
13
- isMacOS: () => process.platform === 'darwin',
14
- isLinux: () => process.platform === 'linux',
15
- isWindows: () => process.platform === 'win32',
16
- getSocketPath: () => join(testDir, 'test.sock'),
17
- getPidPath: () => join(testDir, 'test.pid'),
18
- getDbPath: () => join(testDir, 'test.db'),
19
- 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"),
20
30
  ensureDataDir: () => {},
21
31
  readHttpToken: () => null,
22
32
  }));
23
33
 
24
- mock.module('../util/logger.js', () => ({
34
+ mock.module("../util/logger.js", () => ({
25
35
  getLogger: () =>
26
36
  new Proxy({} as Record<string, unknown>, {
27
37
  get: () => () => {},
@@ -30,26 +40,26 @@ mock.module('../util/logger.js', () => ({
30
40
 
31
41
  // ── Config mock ─────────────────────────────────────────────────────
32
42
 
33
- mock.module('../config/loader.js', () => ({
43
+ mock.module("../config/loader.js", () => ({
34
44
  getConfig: () => ({
35
45
  ui: {},
36
-
37
- provider: 'anthropic',
38
- providerOrder: ['anthropic'],
39
- apiKeys: { anthropic: 'test-key' },
46
+
47
+ provider: "anthropic",
48
+ providerOrder: ["anthropic"],
49
+ apiKeys: { anthropic: "test-key" },
40
50
  calls: {
41
51
  enabled: true,
42
- provider: 'twilio',
52
+ provider: "twilio",
43
53
  maxDurationSeconds: 12 * 60,
44
54
  userConsultTimeoutSeconds: 90,
45
55
  userConsultationTimeoutSeconds: 90,
46
56
  silenceTimeoutSeconds: 30,
47
- disclosure: { enabled: false, text: '' },
57
+ disclosure: { enabled: false, text: "" },
48
58
  safety: { denyCategories: [] },
49
59
  model: undefined,
50
60
  },
51
61
  memory: { enabled: false },
52
- notifications: { decisionModelIntent: 'latency-optimized' },
62
+ notifications: { decisionModelIntent: "latency-optimized" },
53
63
  }),
54
64
  }));
55
65
 
@@ -58,7 +68,7 @@ mock.module('../config/loader.js', () => ({
58
68
  let mockConsultationTimeoutMs = 90_000;
59
69
  let mockSilenceTimeoutMs = 30_000;
60
70
 
61
- mock.module('../calls/call-constants.js', () => ({
71
+ mock.module("../calls/call-constants.js", () => ({
62
72
  getMaxCallDurationMs: () => 12 * 60 * 1000,
63
73
  getUserConsultationTimeoutMs: () => mockConsultationTimeoutMs,
64
74
  getSilenceTimeoutMs: () => mockSilenceTimeoutMs,
@@ -85,8 +95,8 @@ function createMockVoiceTurn(tokens: string[]) {
85
95
  }) => {
86
96
  // Check for abort before proceeding
87
97
  if (opts.signal?.aborted) {
88
- const err = new Error('aborted');
89
- err.name = 'AbortError';
98
+ const err = new Error("aborted");
99
+ err.name = "AbortError";
90
100
  throw err;
91
101
  }
92
102
 
@@ -107,11 +117,10 @@ function createMockVoiceTurn(tokens: string[]) {
107
117
  };
108
118
  }
109
119
 
110
-
111
120
  let mockStartVoiceTurn: Mock<any>;
112
121
 
113
- mock.module('../calls/voice-session-bridge.js', () => {
114
- mockStartVoiceTurn = mock(createMockVoiceTurn(['Hello', ' there']));
122
+ mock.module("../calls/voice-session-bridge.js", () => {
123
+ mockStartVoiceTurn = mock(createMockVoiceTurn(["Hello", " there"]));
115
124
  return {
116
125
  startVoiceTurn: (...args: unknown[]) => mockStartVoiceTurn(...args),
117
126
  setVoiceBridgeDeps: () => {},
@@ -120,25 +129,23 @@ mock.module('../calls/voice-session-bridge.js', () => {
120
129
 
121
130
  // ── Import source modules after all mocks are registered ────────────
122
131
 
123
- import { CallController } from '../calls/call-controller.js';
124
- import {
125
- getCallController,
126
- } from '../calls/call-state.js';
132
+ import { CallController } from "../calls/call-controller.js";
133
+ import { getCallController } from "../calls/call-state.js";
127
134
  import {
128
135
  createCallSession,
129
136
  getCallEvents,
130
137
  getCallSession,
131
138
  getPendingQuestion,
132
139
  updateCallSession,
133
- } from '../calls/call-store.js';
134
- import type { RelayConnection } from '../calls/relay-server.js';
140
+ } from "../calls/call-store.js";
141
+ import type { RelayConnection } from "../calls/relay-server.js";
135
142
  import {
136
143
  getCanonicalGuardianRequest,
137
144
  getPendingCanonicalRequestByCallSessionId,
138
- } from '../memory/canonical-guardian-store.js';
139
- import { getMessages } from '../memory/conversation-store.js';
140
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
141
- import { conversations } from '../memory/schema.js';
145
+ } from "../memory/canonical-guardian-store.js";
146
+ import { getMessages } from "../memory/conversation-store.js";
147
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
148
+ import { conversations } from "../memory/schema.js";
142
149
 
143
150
  initializeDb();
144
151
 
@@ -165,15 +172,25 @@ function createMockRelay(): MockRelay {
165
172
  sentTokens: [] as Array<{ token: string; last: boolean }>,
166
173
  _endCalled: false,
167
174
  _endReason: undefined as string | undefined,
168
- _connectionState: 'connected',
175
+ _connectionState: "connected",
169
176
  };
170
177
 
171
178
  return {
172
- get sentTokens() { return state.sentTokens; },
173
- get endCalled() { return state._endCalled; },
174
- get endReason() { return state._endReason; },
175
- get mockConnectionState() { return state._connectionState; },
176
- set mockConnectionState(v: string) { state._connectionState = v; },
179
+ get sentTokens() {
180
+ return state.sentTokens;
181
+ },
182
+ get endCalled() {
183
+ return state._endCalled;
184
+ },
185
+ get endReason() {
186
+ return state._endReason;
187
+ },
188
+ get mockConnectionState() {
189
+ return state._connectionState;
190
+ },
191
+ set mockConnectionState(v: string) {
192
+ state._connectionState = v;
193
+ },
177
194
  sendTextToken(token: string, last: boolean) {
178
195
  state.sentTokens.push({ token, last });
179
196
  },
@@ -194,91 +211,121 @@ function ensureConversation(id: string): void {
194
211
  if (ensuredConvIds.has(id)) return;
195
212
  const db = getDb();
196
213
  const now = Date.now();
197
- db.insert(conversations).values({
198
- id,
199
- title: `Test conversation ${id}`,
200
- createdAt: now,
201
- updatedAt: now,
202
- }).run();
214
+ db.insert(conversations)
215
+ .values({
216
+ id,
217
+ title: `Test conversation ${id}`,
218
+ createdAt: now,
219
+ updatedAt: now,
220
+ })
221
+ .run();
203
222
  ensuredConvIds.add(id);
204
223
  }
205
224
 
206
225
  function resetTables() {
207
226
  const db = getDb();
208
- db.run('DELETE FROM canonical_guardian_deliveries');
209
- db.run('DELETE FROM canonical_guardian_requests');
210
- db.run('DELETE FROM guardian_action_deliveries');
211
- db.run('DELETE FROM guardian_action_requests');
212
- db.run('DELETE FROM call_pending_questions');
213
- db.run('DELETE FROM call_events');
214
- db.run('DELETE FROM call_sessions');
215
- db.run('DELETE FROM tool_invocations');
216
- db.run('DELETE FROM messages');
217
- db.run('DELETE FROM conversations');
227
+ db.run("DELETE FROM canonical_guardian_deliveries");
228
+ db.run("DELETE FROM canonical_guardian_requests");
229
+ db.run("DELETE FROM guardian_action_deliveries");
230
+ db.run("DELETE FROM guardian_action_requests");
231
+ db.run("DELETE FROM call_pending_questions");
232
+ db.run("DELETE FROM call_events");
233
+ db.run("DELETE FROM call_sessions");
234
+ db.run("DELETE FROM tool_invocations");
235
+ db.run("DELETE FROM messages");
236
+ db.run("DELETE FROM conversations");
218
237
  ensuredConvIds = new Set();
219
238
  }
220
239
 
221
240
  /**
222
241
  * Create a call session and a controller wired to a mock relay.
223
242
  */
224
- function setupController(task?: string, opts?: { assistantId?: string; guardianContext?: import('../daemon/session-runtime-assembly.js').GuardianRuntimeContext }) {
225
- ensureConversation('conv-ctrl-test');
243
+ function setupController(
244
+ task?: string,
245
+ opts?: {
246
+ assistantId?: string;
247
+ guardianContext?: import("../daemon/session-runtime-assembly.js").GuardianRuntimeContext;
248
+ },
249
+ ) {
250
+ ensureConversation("conv-ctrl-test");
226
251
  const session = createCallSession({
227
- conversationId: 'conv-ctrl-test',
228
- provider: 'twilio',
229
- fromNumber: '+15551111111',
230
- toNumber: '+15552222222',
252
+ conversationId: "conv-ctrl-test",
253
+ provider: "twilio",
254
+ fromNumber: "+15551111111",
255
+ toNumber: "+15552222222",
231
256
  task,
232
257
  });
233
- updateCallSession(session.id, { status: 'in_progress' });
258
+ updateCallSession(session.id, { status: "in_progress" });
234
259
  const relay = createMockRelay();
235
- const controller = new CallController(session.id, relay as unknown as RelayConnection, task ?? null, {
236
- assistantId: opts?.assistantId,
237
- guardianContext: opts?.guardianContext,
238
- });
260
+ const controller = new CallController(
261
+ session.id,
262
+ relay as unknown as RelayConnection,
263
+ task ?? null,
264
+ {
265
+ assistantId: opts?.assistantId,
266
+ guardianContext: opts?.guardianContext,
267
+ },
268
+ );
239
269
  return { session, relay, controller };
240
270
  }
241
271
 
242
272
  function getLatestAssistantText(conversationId: string): string | null {
243
- const msgs = getMessages(conversationId).filter((m) => m.role === 'assistant');
273
+ const msgs = getMessages(conversationId).filter(
274
+ (m) => m.role === "assistant",
275
+ );
244
276
  if (msgs.length === 0) return null;
245
277
  const latest = msgs[msgs.length - 1];
246
278
  try {
247
279
  const parsed = JSON.parse(latest.content) as unknown;
248
280
  if (Array.isArray(parsed)) {
249
281
  return parsed
250
- .filter((b): b is { type: string; text?: string } => typeof b === 'object' && b != null)
251
- .filter((b) => b.type === 'text')
252
- .map((b) => b.text ?? '')
253
- .join('');
282
+ .filter(
283
+ (b): b is { type: string; text?: string } =>
284
+ typeof b === "object" && b != null,
285
+ )
286
+ .filter((b) => b.type === "text")
287
+ .map((b) => b.text ?? "")
288
+ .join("");
254
289
  }
255
- if (typeof parsed === 'string') return parsed;
256
- } catch { /* fall through */ }
290
+ if (typeof parsed === "string") return parsed;
291
+ } catch {
292
+ /* fall through */
293
+ }
257
294
  return latest.content;
258
295
  }
259
296
 
260
297
  function setupControllerWithOrigin(task?: string) {
261
- ensureConversation('conv-ctrl-voice');
262
- ensureConversation('conv-ctrl-origin');
298
+ ensureConversation("conv-ctrl-voice");
299
+ ensureConversation("conv-ctrl-origin");
263
300
  const session = createCallSession({
264
- conversationId: 'conv-ctrl-voice',
265
- provider: 'twilio',
266
- fromNumber: '+15551111111',
267
- toNumber: '+15552222222',
301
+ conversationId: "conv-ctrl-voice",
302
+ provider: "twilio",
303
+ fromNumber: "+15551111111",
304
+ toNumber: "+15552222222",
268
305
  task,
269
- initiatedFromConversationId: 'conv-ctrl-origin',
306
+ initiatedFromConversationId: "conv-ctrl-origin",
307
+ });
308
+ updateCallSession(session.id, {
309
+ status: "in_progress",
310
+ startedAt: Date.now() - 30_000,
270
311
  });
271
- updateCallSession(session.id, { status: 'in_progress', startedAt: Date.now() - 30_000 });
272
312
  const relay = createMockRelay();
273
- const controller = new CallController(session.id, relay as unknown as RelayConnection, task ?? null, {});
313
+ const controller = new CallController(
314
+ session.id,
315
+ relay as unknown as RelayConnection,
316
+ task ?? null,
317
+ {},
318
+ );
274
319
  return { session, relay, controller };
275
320
  }
276
321
 
277
- describe('call-controller', () => {
322
+ describe("call-controller", () => {
278
323
  beforeEach(() => {
279
324
  resetTables();
280
325
  // Reset the bridge mock to default behaviour
281
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Hello', ' there']));
326
+ mockStartVoiceTurn.mockImplementation(
327
+ createMockVoiceTurn(["Hello", " there"]),
328
+ );
282
329
  // Reset consultation timeout to the default (long) value
283
330
  mockConsultationTimeoutMs = 90_000;
284
331
  mockSilenceTimeoutMs = 30_000;
@@ -286,11 +333,13 @@ describe('call-controller', () => {
286
333
 
287
334
  // ── handleCallerUtterance ─────────────────────────────────────────
288
335
 
289
- test('handleCallerUtterance: streams tokens via sendTextToken', async () => {
290
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Hi', ', how', ' are you?']));
336
+ test("handleCallerUtterance: streams tokens via sendTextToken", async () => {
337
+ mockStartVoiceTurn.mockImplementation(
338
+ createMockVoiceTurn(["Hi", ", how", " are you?"]),
339
+ );
291
340
  const { relay, controller } = setupController();
292
341
 
293
- await controller.handleCallerUtterance('Hello');
342
+ await controller.handleCallerUtterance("Hello");
294
343
 
295
344
  // Verify tokens were sent to the relay
296
345
  const nonEmptyTokens = relay.sentTokens.filter((t) => t.token.length > 0);
@@ -302,11 +351,13 @@ describe('call-controller', () => {
302
351
  controller.destroy();
303
352
  });
304
353
 
305
- test('handleCallerUtterance: sends last=true at end of turn', async () => {
306
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Simple response.']));
354
+ test("handleCallerUtterance: sends last=true at end of turn", async () => {
355
+ mockStartVoiceTurn.mockImplementation(
356
+ createMockVoiceTurn(["Simple response."]),
357
+ );
307
358
  const { relay, controller } = setupController();
308
359
 
309
- await controller.handleCallerUtterance('Test');
360
+ await controller.handleCallerUtterance("Test");
310
361
 
311
362
  // Find the final empty-string token that marks end of turn
312
363
  const endMarkers = relay.sentTokens.filter((t) => t.last === true);
@@ -315,113 +366,145 @@ describe('call-controller', () => {
315
366
  controller.destroy();
316
367
  });
317
368
 
318
- test('handleCallerUtterance: includes speaker context in voice turn content', async () => {
319
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
320
- expect(opts.content).toContain('[SPEAKER id="speaker-1" label="Aaron" source="provider" confidence="0.91"]');
321
- expect(opts.content).toContain('Can you summarize this meeting?');
322
- opts.onTextDelta('Sure, here is a summary.');
323
- opts.onComplete();
324
- return { turnId: 'run-1', abort: () => {} };
325
- });
369
+ test("handleCallerUtterance: includes speaker context in voice turn content", async () => {
370
+ mockStartVoiceTurn.mockImplementation(
371
+ async (opts: {
372
+ content: string;
373
+ onTextDelta: (t: string) => void;
374
+ onComplete: () => void;
375
+ }) => {
376
+ expect(opts.content).toContain(
377
+ '[SPEAKER id="speaker-1" label="Aaron" source="provider" confidence="0.91"]',
378
+ );
379
+ expect(opts.content).toContain("Can you summarize this meeting?");
380
+ opts.onTextDelta("Sure, here is a summary.");
381
+ opts.onComplete();
382
+ return { turnId: "run-1", abort: () => {} };
383
+ },
384
+ );
326
385
 
327
386
  const { controller } = setupController();
328
387
 
329
- await controller.handleCallerUtterance('Can you summarize this meeting?', {
330
- speakerId: 'speaker-1',
331
- speakerLabel: 'Aaron',
388
+ await controller.handleCallerUtterance("Can you summarize this meeting?", {
389
+ speakerId: "speaker-1",
390
+ speakerLabel: "Aaron",
332
391
  speakerConfidence: 0.91,
333
- source: 'provider',
392
+ source: "provider",
334
393
  });
335
394
 
336
395
  controller.destroy();
337
396
  });
338
397
 
339
- test('startInitialGreeting: sends CALL_OPENING content and strips control marker from speech', async () => {
398
+ test("startInitialGreeting: sends CALL_OPENING content and strips control marker from speech", async () => {
340
399
  let turnCount = 0;
341
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
342
- turnCount++;
343
- expect(opts.content).toContain('[CALL_OPENING]');
344
- const tokens = ['Hi, I am calling about your appointment request. Is now a good time to talk?'];
345
- for (const token of tokens) {
346
- opts.onTextDelta(token);
347
- }
348
- opts.onComplete();
349
- return { turnId: 'run-1', abort: () => {} };
350
- });
400
+ mockStartVoiceTurn.mockImplementation(
401
+ async (opts: {
402
+ content: string;
403
+ onTextDelta: (t: string) => void;
404
+ onComplete: () => void;
405
+ }) => {
406
+ turnCount++;
407
+ expect(opts.content).toContain("[CALL_OPENING]");
408
+ const tokens = [
409
+ "Hi, I am calling about your appointment request. Is now a good time to talk?",
410
+ ];
411
+ for (const token of tokens) {
412
+ opts.onTextDelta(token);
413
+ }
414
+ opts.onComplete();
415
+ return { turnId: "run-1", abort: () => {} };
416
+ },
417
+ );
351
418
 
352
- const { relay, controller } = setupController('Confirm appointment');
419
+ const { relay, controller } = setupController("Confirm appointment");
353
420
 
354
421
  await controller.startInitialGreeting();
355
422
  await controller.startInitialGreeting(); // should be no-op
356
423
 
357
- const allText = relay.sentTokens.map((t) => t.token).join('');
358
- expect(allText).toContain('appointment request');
359
- expect(allText).toContain('good time to talk');
360
- expect(allText).not.toContain('[CALL_OPENING]');
424
+ const allText = relay.sentTokens.map((t) => t.token).join("");
425
+ expect(allText).toContain("appointment request");
426
+ expect(allText).toContain("good time to talk");
427
+ expect(allText).not.toContain("[CALL_OPENING]");
361
428
  expect(turnCount).toBe(1); // idempotent
362
429
 
363
430
  controller.destroy();
364
431
  });
365
432
 
366
- test('startInitialGreeting: tags only the first caller response with CALL_OPENING_ACK', async () => {
433
+ test("startInitialGreeting: tags only the first caller response with CALL_OPENING_ACK", async () => {
367
434
  let turnCount = 0;
368
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
369
- turnCount++;
370
-
371
- let tokens: string[];
372
- if (turnCount === 1) {
373
- expect(opts.content).toContain('[CALL_OPENING]');
374
- tokens = ['Hey Noa, it\'s Credence calling about your joke request. Is now okay for a quick one?'];
375
- } else if (turnCount === 2) {
376
- expect(opts.content).toContain('[CALL_OPENING_ACK]');
377
- expect(opts.content).toContain('Yeah. Sure. What\'s up?');
378
- tokens = ['Great, here\'s one right away. Why did the scarecrow win an award?'];
379
- } else {
380
- expect(opts.content).not.toContain('[CALL_OPENING_ACK]');
381
- expect(opts.content).toContain('Tell me the punchline');
382
- tokens = ['Because he was outstanding in his field.'];
383
- }
384
-
385
- for (const token of tokens) {
386
- opts.onTextDelta(token);
387
- }
388
- opts.onComplete();
389
- return { turnId: `run-${turnCount}`, abort: () => {} };
390
- });
435
+ mockStartVoiceTurn.mockImplementation(
436
+ async (opts: {
437
+ content: string;
438
+ onTextDelta: (t: string) => void;
439
+ onComplete: () => void;
440
+ }) => {
441
+ turnCount++;
442
+
443
+ let tokens: string[];
444
+ if (turnCount === 1) {
445
+ expect(opts.content).toContain("[CALL_OPENING]");
446
+ tokens = [
447
+ "Hey Noa, it's Credence calling about your joke request. Is now okay for a quick one?",
448
+ ];
449
+ } else if (turnCount === 2) {
450
+ expect(opts.content).toContain("[CALL_OPENING_ACK]");
451
+ expect(opts.content).toContain("Yeah. Sure. What's up?");
452
+ tokens = [
453
+ "Great, here's one right away. Why did the scarecrow win an award?",
454
+ ];
455
+ } else {
456
+ expect(opts.content).not.toContain("[CALL_OPENING_ACK]");
457
+ expect(opts.content).toContain("Tell me the punchline");
458
+ tokens = ["Because he was outstanding in his field."];
459
+ }
460
+
461
+ for (const token of tokens) {
462
+ opts.onTextDelta(token);
463
+ }
464
+ opts.onComplete();
465
+ return { turnId: `run-${turnCount}`, abort: () => {} };
466
+ },
467
+ );
391
468
 
392
- const { controller } = setupController('Tell a joke immediately');
469
+ const { controller } = setupController("Tell a joke immediately");
393
470
 
394
471
  await controller.startInitialGreeting();
395
- await controller.handleCallerUtterance('Yeah. Sure. What\'s up?');
396
- await controller.handleCallerUtterance('Tell me the punchline');
472
+ await controller.handleCallerUtterance("Yeah. Sure. What's up?");
473
+ await controller.handleCallerUtterance("Tell me the punchline");
397
474
 
398
475
  expect(turnCount).toBe(3);
399
476
 
400
477
  controller.destroy();
401
478
  });
402
479
 
403
- test('markNextCallerTurnAsOpeningAck: tags the next caller turn with CALL_OPENING_ACK without requiring a prior CALL_OPENING', async () => {
480
+ test("markNextCallerTurnAsOpeningAck: tags the next caller turn with CALL_OPENING_ACK without requiring a prior CALL_OPENING", async () => {
404
481
  let turnCount = 0;
405
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
406
- turnCount++;
407
-
408
- if (turnCount === 1) {
409
- // First caller utterance after markNextCallerTurnAsOpeningAck
410
- expect(opts.content).toContain('[CALL_OPENING_ACK]');
411
- expect(opts.content).toContain('I want to check my balance');
412
- for (const token of ['Sure, let me check your balance.']) {
413
- opts.onTextDelta(token);
414
- }
415
- } else {
416
- // Subsequent utterance should NOT have the marker
417
- expect(opts.content).not.toContain('[CALL_OPENING_ACK]');
418
- for (const token of ['Your balance is $42.']) {
419
- opts.onTextDelta(token);
482
+ mockStartVoiceTurn.mockImplementation(
483
+ async (opts: {
484
+ content: string;
485
+ onTextDelta: (t: string) => void;
486
+ onComplete: () => void;
487
+ }) => {
488
+ turnCount++;
489
+
490
+ if (turnCount === 1) {
491
+ // First caller utterance after markNextCallerTurnAsOpeningAck
492
+ expect(opts.content).toContain("[CALL_OPENING_ACK]");
493
+ expect(opts.content).toContain("I want to check my balance");
494
+ for (const token of ["Sure, let me check your balance."]) {
495
+ opts.onTextDelta(token);
496
+ }
497
+ } else {
498
+ // Subsequent utterance should NOT have the marker
499
+ expect(opts.content).not.toContain("[CALL_OPENING_ACK]");
500
+ for (const token of ["Your balance is $42."]) {
501
+ opts.onTextDelta(token);
502
+ }
420
503
  }
421
- }
422
- opts.onComplete();
423
- return { turnId: `run-${turnCount}`, abort: () => {} };
424
- });
504
+ opts.onComplete();
505
+ return { turnId: `run-${turnCount}`, abort: () => {} };
506
+ },
507
+ );
425
508
 
426
509
  const { controller } = setupController();
427
510
 
@@ -429,8 +512,8 @@ describe('call-controller', () => {
429
512
  // without any prior startInitialGreeting / CALL_OPENING
430
513
  controller.markNextCallerTurnAsOpeningAck();
431
514
 
432
- await controller.handleCallerUtterance('I want to check my balance');
433
- await controller.handleCallerUtterance('How much exactly?');
515
+ await controller.handleCallerUtterance("I want to check my balance");
516
+ await controller.handleCallerUtterance("How much exactly?");
434
517
 
435
518
  expect(turnCount).toBe(2);
436
519
 
@@ -439,112 +522,123 @@ describe('call-controller', () => {
439
522
 
440
523
  // ── ASK_GUARDIAN pattern ──────────────────────────────────────────
441
524
 
442
- test('ASK_GUARDIAN pattern: detects pattern, creates pending question, sets session to waiting_on_user', async () => {
443
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
444
- ['Let me check on that. ', '[ASK_GUARDIAN: What date works best?]'],
445
- ));
446
- const { session, relay, controller } = setupController('Book appointment');
525
+ test("ASK_GUARDIAN pattern: detects pattern, creates pending question, sets session to waiting_on_user", async () => {
526
+ mockStartVoiceTurn.mockImplementation(
527
+ createMockVoiceTurn([
528
+ "Let me check on that. ",
529
+ "[ASK_GUARDIAN: What date works best?]",
530
+ ]),
531
+ );
532
+ const { session, relay, controller } = setupController("Book appointment");
447
533
 
448
- await controller.handleCallerUtterance('I need to schedule something');
534
+ await controller.handleCallerUtterance("I need to schedule something");
449
535
 
450
536
  // Verify a pending question was created
451
537
  const question = getPendingQuestion(session.id);
452
538
  expect(question).not.toBeNull();
453
- expect(question!.questionText).toBe('What date works best?');
454
- expect(question!.status).toBe('pending');
539
+ expect(question!.questionText).toBe("What date works best?");
540
+ expect(question!.status).toBe("pending");
455
541
 
456
542
  // Controller state returns to idle (non-blocking); consultation is
457
543
  // tracked separately via pendingConsultation.
458
- expect(controller.getState()).toBe('idle');
544
+ expect(controller.getState()).toBe("idle");
459
545
 
460
546
  // Session status in the store is still set to waiting_on_user for
461
547
  // external consumers (e.g. the answer route).
462
548
  const updatedSession = getCallSession(session.id);
463
- expect(updatedSession!.status).toBe('waiting_on_user');
549
+ expect(updatedSession!.status).toBe("waiting_on_user");
464
550
 
465
551
  // A pending consultation should be active
466
552
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
467
553
 
468
554
  // The ASK_GUARDIAN marker text should NOT appear in the relay tokens
469
- const allText = relay.sentTokens.map((t) => t.token).join('');
470
- expect(allText).not.toContain('[ASK_GUARDIAN:');
555
+ const allText = relay.sentTokens.map((t) => t.token).join("");
556
+ expect(allText).not.toContain("[ASK_GUARDIAN:");
471
557
 
472
558
  controller.destroy();
473
559
  });
474
560
 
475
- test('strips internal context markers from spoken output', async () => {
476
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn([
477
- 'Thanks for waiting. ',
478
- '[USER_ANSWERED: The guardian said 3 PM works.] ',
479
- '[USER_INSTRUCTION: Keep this short.] ',
480
- '[CALL_OPENING_ACK] ',
481
- 'I can confirm 3 PM works.',
482
- ]));
561
+ test("strips internal context markers from spoken output", async () => {
562
+ mockStartVoiceTurn.mockImplementation(
563
+ createMockVoiceTurn([
564
+ "Thanks for waiting. ",
565
+ "[USER_ANSWERED: The guardian said 3 PM works.] ",
566
+ "[USER_INSTRUCTION: Keep this short.] ",
567
+ "[CALL_OPENING_ACK] ",
568
+ "I can confirm 3 PM works.",
569
+ ]),
570
+ );
483
571
  const { relay, controller } = setupController();
484
572
 
485
- await controller.handleCallerUtterance('Any update?');
573
+ await controller.handleCallerUtterance("Any update?");
486
574
 
487
- const allText = relay.sentTokens.map((t) => t.token).join('');
488
- expect(allText).toContain('Thanks for waiting.');
489
- expect(allText).toContain('I can confirm 3 PM works.');
490
- expect(allText).not.toContain('[USER_ANSWERED:');
491
- expect(allText).not.toContain('[USER_INSTRUCTION:');
492
- expect(allText).not.toContain('[CALL_OPENING_ACK]');
493
- expect(allText).not.toContain('USER_ANSWERED');
494
- expect(allText).not.toContain('USER_INSTRUCTION');
495
- expect(allText).not.toContain('CALL_OPENING_ACK');
575
+ const allText = relay.sentTokens.map((t) => t.token).join("");
576
+ expect(allText).toContain("Thanks for waiting.");
577
+ expect(allText).toContain("I can confirm 3 PM works.");
578
+ expect(allText).not.toContain("[USER_ANSWERED:");
579
+ expect(allText).not.toContain("[USER_INSTRUCTION:");
580
+ expect(allText).not.toContain("[CALL_OPENING_ACK]");
581
+ expect(allText).not.toContain("USER_ANSWERED");
582
+ expect(allText).not.toContain("USER_INSTRUCTION");
583
+ expect(allText).not.toContain("CALL_OPENING_ACK");
496
584
 
497
585
  controller.destroy();
498
586
  });
499
587
 
500
588
  // ── END_CALL pattern ──────────────────────────────────────────────
501
589
 
502
- test('END_CALL pattern: detects marker, calls endSession, updates status to completed', async () => {
503
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
504
- ['Thank you for calling, goodbye! ', '[END_CALL]'],
505
- ));
590
+ test("END_CALL pattern: detects marker, calls endSession, updates status to completed", async () => {
591
+ mockStartVoiceTurn.mockImplementation(
592
+ createMockVoiceTurn(["Thank you for calling, goodbye! ", "[END_CALL]"]),
593
+ );
506
594
  const { session, relay, controller } = setupController();
507
595
 
508
- await controller.handleCallerUtterance('That is all, thanks');
596
+ await controller.handleCallerUtterance("That is all, thanks");
509
597
 
510
598
  // endSession should have been called
511
599
  expect(relay.endCalled).toBe(true);
512
600
 
513
601
  // Session status should be completed
514
602
  const updatedSession = getCallSession(session.id);
515
- expect(updatedSession!.status).toBe('completed');
603
+ expect(updatedSession!.status).toBe("completed");
516
604
  expect(updatedSession!.endedAt).not.toBeNull();
517
605
 
518
606
  // The END_CALL marker text should NOT appear in the relay tokens
519
- const allText = relay.sentTokens.map((t) => t.token).join('');
520
- expect(allText).not.toContain('[END_CALL]');
607
+ const allText = relay.sentTokens.map((t) => t.token).join("");
608
+ expect(allText).not.toContain("[END_CALL]");
521
609
 
522
610
  controller.destroy();
523
611
  });
524
612
 
525
613
  // ── handleUserAnswer ──────────────────────────────────────────────
526
614
 
527
- test('handleUserAnswer: returns true immediately and fires LLM asynchronously', async () => {
615
+ test("handleUserAnswer: returns true immediately and fires LLM asynchronously", async () => {
528
616
  // First utterance triggers ASK_GUARDIAN
529
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
530
- ['Hold on. [ASK_GUARDIAN: Preferred time?]'],
531
- ));
617
+ mockStartVoiceTurn.mockImplementation(
618
+ createMockVoiceTurn(["Hold on. [ASK_GUARDIAN: Preferred time?]"]),
619
+ );
532
620
  const { relay, controller } = setupController();
533
621
 
534
- await controller.handleCallerUtterance('I need an appointment');
622
+ await controller.handleCallerUtterance("I need an appointment");
535
623
 
536
624
  // Now provide the answer — reset mock for second turn
537
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
538
- expect(opts.content).toContain('[USER_ANSWERED: 3pm tomorrow]');
539
- const tokens = ['Great, I have scheduled for 3pm tomorrow.'];
540
- for (const token of tokens) {
541
- opts.onTextDelta(token);
542
- }
543
- opts.onComplete();
544
- return { turnId: 'run-2', abort: () => {} };
545
- });
625
+ mockStartVoiceTurn.mockImplementation(
626
+ async (opts: {
627
+ content: string;
628
+ onTextDelta: (t: string) => void;
629
+ onComplete: () => void;
630
+ }) => {
631
+ expect(opts.content).toContain("[USER_ANSWERED: 3pm tomorrow]");
632
+ const tokens = ["Great, I have scheduled for 3pm tomorrow."];
633
+ for (const token of tokens) {
634
+ opts.onTextDelta(token);
635
+ }
636
+ opts.onComplete();
637
+ return { turnId: "run-2", abort: () => {} };
638
+ },
639
+ );
546
640
 
547
- const accepted = await controller.handleUserAnswer('3pm tomorrow');
641
+ const accepted = await controller.handleUserAnswer("3pm tomorrow");
548
642
  expect(accepted).toBe(true);
549
643
 
550
644
  // handleUserAnswer fires runTurn without awaiting, so give the
@@ -552,7 +646,9 @@ describe('call-controller', () => {
552
646
  await new Promise((r) => setTimeout(r, 50));
553
647
 
554
648
  // Should have streamed a response for the answer
555
- const tokensAfterAnswer = relay.sentTokens.filter((t) => t.token.includes('3pm'));
649
+ const tokensAfterAnswer = relay.sentTokens.filter((t) =>
650
+ t.token.includes("3pm"),
651
+ );
556
652
  expect(tokensAfterAnswer.length).toBeGreaterThan(0);
557
653
 
558
654
  controller.destroy();
@@ -560,33 +656,42 @@ describe('call-controller', () => {
560
656
 
561
657
  // ── Full mid-call question flow ──────────────────────────────────
562
658
 
563
- test('mid-call question flow: unavailable time -> ask user -> user confirms -> resumed call', async () => {
659
+ test("mid-call question flow: unavailable time -> ask user -> user confirms -> resumed call", async () => {
564
660
  // Step 1: Caller says "7:30" but it's unavailable. The LLM asks the user.
565
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
566
- ['I\'m sorry, 7:30 is not available. ', '[ASK_GUARDIAN: Is 8:00 okay instead?]'],
567
- ));
661
+ mockStartVoiceTurn.mockImplementation(
662
+ createMockVoiceTurn([
663
+ "I'm sorry, 7:30 is not available. ",
664
+ "[ASK_GUARDIAN: Is 8:00 okay instead?]",
665
+ ]),
666
+ );
568
667
 
569
- const { session, relay, controller } = setupController('Schedule a haircut');
668
+ const { session, relay, controller } =
669
+ setupController("Schedule a haircut");
570
670
 
571
- await controller.handleCallerUtterance('Can I book for 7:30?');
671
+ await controller.handleCallerUtterance("Can I book for 7:30?");
572
672
 
573
673
  // Controller returns to idle (non-blocking); consultation tracked separately
574
- expect(controller.getState()).toBe('idle');
674
+ expect(controller.getState()).toBe("idle");
575
675
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
576
676
  const question = getPendingQuestion(session.id);
577
677
  expect(question).not.toBeNull();
578
- expect(question!.questionText).toBe('Is 8:00 okay instead?');
678
+ expect(question!.questionText).toBe("Is 8:00 okay instead?");
579
679
 
580
680
  // Session status in store reflects consultation state
581
681
  const midSession = getCallSession(session.id);
582
- expect(midSession!.status).toBe('waiting_on_user');
682
+ expect(midSession!.status).toBe("waiting_on_user");
583
683
 
584
684
  // Step 2: User answers "Yes, 8:00 works"
585
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
586
- ['Great, I\'ve booked you for 8:00. See you then! ', '[END_CALL]'],
587
- ));
685
+ mockStartVoiceTurn.mockImplementation(
686
+ createMockVoiceTurn([
687
+ "Great, I've booked you for 8:00. See you then! ",
688
+ "[END_CALL]",
689
+ ]),
690
+ );
588
691
 
589
- const accepted = await controller.handleUserAnswer('Yes, 8:00 works for me');
692
+ const accepted = await controller.handleUserAnswer(
693
+ "Yes, 8:00 works for me",
694
+ );
590
695
  expect(accepted).toBe(true);
591
696
 
592
697
  // Give the fire-and-forget LLM call time to complete
@@ -594,7 +699,7 @@ describe('call-controller', () => {
594
699
 
595
700
  // Step 3: Verify call completed
596
701
  const endSession = getCallSession(session.id);
597
- expect(endSession!.status).toBe('completed');
702
+ expect(endSession!.status).toBe("completed");
598
703
  expect(endSession!.endedAt).not.toBeNull();
599
704
 
600
705
  // Verify the END_CALL marker triggered endSession on relay
@@ -605,33 +710,35 @@ describe('call-controller', () => {
605
710
 
606
711
  // ── Error handling ────────────────────────────────────────────────
607
712
 
608
- test('Voice turn error: sends error message to caller and returns to idle', async () => {
609
- mockStartVoiceTurn.mockImplementation(async (opts: { onError: (msg: string) => void }) => {
610
- opts.onError('API rate limit exceeded');
611
- return { turnId: 'run-err', abort: () => {} };
612
- });
713
+ test("Voice turn error: sends error message to caller and returns to idle", async () => {
714
+ mockStartVoiceTurn.mockImplementation(
715
+ async (opts: { onError: (msg: string) => void }) => {
716
+ opts.onError("API rate limit exceeded");
717
+ return { turnId: "run-err", abort: () => {} };
718
+ },
719
+ );
613
720
 
614
721
  const { relay, controller } = setupController();
615
722
 
616
- await controller.handleCallerUtterance('Hello');
723
+ await controller.handleCallerUtterance("Hello");
617
724
 
618
725
  // Should have sent an error recovery message
619
726
  const errorTokens = relay.sentTokens.filter((t) =>
620
- t.token.includes('technical issue'),
727
+ t.token.includes("technical issue"),
621
728
  );
622
729
  expect(errorTokens.length).toBeGreaterThan(0);
623
730
 
624
731
  // State should return to idle after error
625
- expect(controller.getState()).toBe('idle');
732
+ expect(controller.getState()).toBe("idle");
626
733
 
627
734
  controller.destroy();
628
735
  });
629
736
 
630
- test('handleUserAnswer: returns false when no pending consultation exists', async () => {
737
+ test("handleUserAnswer: returns false when no pending consultation exists", async () => {
631
738
  const { controller } = setupController();
632
739
 
633
740
  // No consultation is pending — answer should be rejected
634
- const result = await controller.handleUserAnswer('some answer');
741
+ const result = await controller.handleUserAnswer("some answer");
635
742
  expect(result).toBe(false);
636
743
 
637
744
  controller.destroy();
@@ -639,7 +746,7 @@ describe('call-controller', () => {
639
746
 
640
747
  // ── handleInterrupt ───────────────────────────────────────────────
641
748
 
642
- test('handleInterrupt: resets state to idle', () => {
749
+ test("handleInterrupt: resets state to idle", () => {
643
750
  const { controller } = setupController();
644
751
 
645
752
  // Calling handleInterrupt should not throw
@@ -648,163 +755,197 @@ describe('call-controller', () => {
648
755
  controller.destroy();
649
756
  });
650
757
 
651
- test('handleInterrupt: sends turn terminator when interrupting active speech', async () => {
652
- mockStartVoiceTurn.mockImplementation(async (opts: { signal?: AbortSignal; onTextDelta: (t: string) => void; onComplete: () => void }) => {
653
- return new Promise((resolve) => {
654
- // Simulate a long-running turn that can be aborted
655
- const timeout = setTimeout(() => {
656
- opts.onTextDelta('This should be interrupted');
657
- opts.onComplete();
658
- resolve({ turnId: 'run-1', abort: () => {} });
659
- }, 1000);
660
-
661
- opts.signal?.addEventListener('abort', () => {
662
- clearTimeout(timeout);
663
- // In the real system, generation_cancelled triggers
664
- // onComplete via the event sink. The AbortSignal listener
665
- // in call-controller also resolves turnComplete defensively.
666
- opts.onComplete();
667
- resolve({ turnId: 'run-1', abort: () => {} });
668
- }, { once: true });
669
- });
670
- });
758
+ test("handleInterrupt: sends turn terminator when interrupting active speech", async () => {
759
+ mockStartVoiceTurn.mockImplementation(
760
+ async (opts: {
761
+ signal?: AbortSignal;
762
+ onTextDelta: (t: string) => void;
763
+ onComplete: () => void;
764
+ }) => {
765
+ return new Promise((resolve) => {
766
+ // Simulate a long-running turn that can be aborted
767
+ const timeout = setTimeout(() => {
768
+ opts.onTextDelta("This should be interrupted");
769
+ opts.onComplete();
770
+ resolve({ turnId: "run-1", abort: () => {} });
771
+ }, 1000);
772
+
773
+ opts.signal?.addEventListener(
774
+ "abort",
775
+ () => {
776
+ clearTimeout(timeout);
777
+ // In the real system, generation_cancelled triggers
778
+ // onComplete via the event sink. The AbortSignal listener
779
+ // in call-controller also resolves turnComplete defensively.
780
+ opts.onComplete();
781
+ resolve({ turnId: "run-1", abort: () => {} });
782
+ },
783
+ { once: true },
784
+ );
785
+ });
786
+ },
787
+ );
671
788
 
672
789
  const { relay, controller } = setupController();
673
- const turnPromise = controller.handleCallerUtterance('Start speaking');
790
+ const turnPromise = controller.handleCallerUtterance("Start speaking");
674
791
  await new Promise((r) => setTimeout(r, 5));
675
792
  controller.handleInterrupt();
676
793
  await turnPromise;
677
794
 
678
- const endTurnMarkers = relay.sentTokens.filter((t) => t.token === '' && t.last === true);
795
+ const endTurnMarkers = relay.sentTokens.filter(
796
+ (t) => t.token === "" && t.last === true,
797
+ );
679
798
  expect(endTurnMarkers.length).toBeGreaterThan(0);
680
799
 
681
800
  controller.destroy();
682
801
  });
683
802
 
684
- test('handleInterrupt: turnComplete settles even when event sink callbacks are not called', async () => {
803
+ test("handleInterrupt: turnComplete settles even when event sink callbacks are not called", async () => {
685
804
  // Simulate a turn that never calls onComplete or onError on abort —
686
805
  // the defensive AbortSignal listener in runTurn() should settle the promise.
687
- mockStartVoiceTurn.mockImplementation(async (opts: { signal?: AbortSignal; onTextDelta: (t: string) => void; onComplete: () => void }) => {
688
- return new Promise((resolve) => {
689
- const timeout = setTimeout(() => {
690
- opts.onTextDelta('Long running turn');
691
- opts.onComplete();
692
- resolve({ turnId: 'run-1', abort: () => {} });
693
- }, 5000);
694
-
695
- opts.signal?.addEventListener('abort', () => {
696
- clearTimeout(timeout);
697
- // Intentionally do NOT call onComplete simulates the old
698
- // broken path where generation_cancelled was not forwarded.
699
- resolve({ turnId: 'run-1', abort: () => {} });
700
- }, { once: true });
701
- });
702
- });
806
+ mockStartVoiceTurn.mockImplementation(
807
+ async (opts: {
808
+ signal?: AbortSignal;
809
+ onTextDelta: (t: string) => void;
810
+ onComplete: () => void;
811
+ }) => {
812
+ return new Promise((resolve) => {
813
+ const timeout = setTimeout(() => {
814
+ opts.onTextDelta("Long running turn");
815
+ opts.onComplete();
816
+ resolve({ turnId: "run-1", abort: () => {} });
817
+ }, 5000);
818
+
819
+ opts.signal?.addEventListener(
820
+ "abort",
821
+ () => {
822
+ clearTimeout(timeout);
823
+ // Intentionally do NOT call onComplete — simulates the old
824
+ // broken path where generation_cancelled was not forwarded.
825
+ resolve({ turnId: "run-1", abort: () => {} });
826
+ },
827
+ { once: true },
828
+ );
829
+ });
830
+ },
831
+ );
703
832
 
704
833
  const { controller } = setupController();
705
- const turnPromise = controller.handleCallerUtterance('Start speaking');
834
+ const turnPromise = controller.handleCallerUtterance("Start speaking");
706
835
  await new Promise((r) => setTimeout(r, 5));
707
836
  controller.handleInterrupt();
708
837
 
709
838
  // Should not hang — the AbortSignal listener resolves the promise
710
839
  await turnPromise;
711
840
 
712
- expect(controller.getState()).toBe('idle');
841
+ expect(controller.getState()).toBe("idle");
713
842
 
714
843
  controller.destroy();
715
844
  });
716
845
 
717
846
  // ── Guardian context pass-through ──────────────────────────────────
718
847
 
719
- test('handleCallerUtterance: passes guardian context to startVoiceTurn', async () => {
848
+ test("handleCallerUtterance: passes guardian context to startVoiceTurn", async () => {
720
849
  const guardianCtx = {
721
- sourceChannel: 'voice' as const,
722
- trustClass: 'trusted_contact' as const,
723
- guardianExternalUserId: '+15550009999',
724
- guardianChatId: '+15550009999',
725
- requesterExternalUserId: '+15550002222',
850
+ sourceChannel: "voice" as const,
851
+ trustClass: "trusted_contact" as const,
852
+ guardianExternalUserId: "+15550009999",
853
+ guardianChatId: "+15550009999",
854
+ requesterExternalUserId: "+15550002222",
726
855
  };
727
856
 
728
857
  let capturedGuardianContext: unknown = undefined;
729
- mockStartVoiceTurn.mockImplementation(async (opts: {
730
- guardianContext?: unknown;
731
- onTextDelta: (t: string) => void;
732
- onComplete: () => void;
733
- }) => {
734
- capturedGuardianContext = opts.guardianContext;
735
- opts.onTextDelta('Hello.');
736
- opts.onComplete();
737
- return { turnId: 'run-gc', abort: () => {} };
738
- });
858
+ mockStartVoiceTurn.mockImplementation(
859
+ async (opts: {
860
+ guardianContext?: unknown;
861
+ onTextDelta: (t: string) => void;
862
+ onComplete: () => void;
863
+ }) => {
864
+ capturedGuardianContext = opts.guardianContext;
865
+ opts.onTextDelta("Hello.");
866
+ opts.onComplete();
867
+ return { turnId: "run-gc", abort: () => {} };
868
+ },
869
+ );
739
870
 
740
- const { controller } = setupController(undefined, { guardianContext: guardianCtx });
871
+ const { controller } = setupController(undefined, {
872
+ guardianContext: guardianCtx,
873
+ });
741
874
 
742
- await controller.handleCallerUtterance('Hello');
875
+ await controller.handleCallerUtterance("Hello");
743
876
 
744
877
  expect(capturedGuardianContext).toEqual(guardianCtx);
745
878
 
746
879
  controller.destroy();
747
880
  });
748
881
 
749
- test('handleCallerUtterance: passes assistantId to startVoiceTurn', async () => {
882
+ test("handleCallerUtterance: passes assistantId to startVoiceTurn", async () => {
750
883
  let capturedAssistantId: string | undefined;
751
- mockStartVoiceTurn.mockImplementation(async (opts: {
752
- assistantId?: string;
753
- onTextDelta: (t: string) => void;
754
- onComplete: () => void;
755
- }) => {
756
- capturedAssistantId = opts.assistantId;
757
- opts.onTextDelta('Hello.');
758
- opts.onComplete();
759
- return { turnId: 'run-aid', abort: () => {} };
760
- });
884
+ mockStartVoiceTurn.mockImplementation(
885
+ async (opts: {
886
+ assistantId?: string;
887
+ onTextDelta: (t: string) => void;
888
+ onComplete: () => void;
889
+ }) => {
890
+ capturedAssistantId = opts.assistantId;
891
+ opts.onTextDelta("Hello.");
892
+ opts.onComplete();
893
+ return { turnId: "run-aid", abort: () => {} };
894
+ },
895
+ );
761
896
 
762
- const { controller } = setupController(undefined, { assistantId: 'my-assistant' });
897
+ const { controller } = setupController(undefined, {
898
+ assistantId: "my-assistant",
899
+ });
763
900
 
764
- await controller.handleCallerUtterance('Hello');
901
+ await controller.handleCallerUtterance("Hello");
765
902
 
766
- expect(capturedAssistantId).toBe('my-assistant');
903
+ expect(capturedAssistantId).toBe("my-assistant");
767
904
 
768
905
  controller.destroy();
769
906
  });
770
907
 
771
- test('setGuardianContext: subsequent turns use updated guardian context', async () => {
908
+ test("setGuardianContext: subsequent turns use updated guardian context", async () => {
772
909
  const initialCtx = {
773
- sourceChannel: 'voice' as const,
774
- trustClass: 'unknown' as const,
775
- denialReason: 'no_binding' as const,
910
+ sourceChannel: "voice" as const,
911
+ trustClass: "unknown" as const,
912
+ denialReason: "no_binding" as const,
776
913
  };
777
914
 
778
915
  const upgradedCtx = {
779
- sourceChannel: 'voice' as const,
780
- trustClass: 'guardian' as const,
781
- guardianExternalUserId: '+15550003333',
782
- guardianChatId: '+15550003333',
916
+ sourceChannel: "voice" as const,
917
+ trustClass: "guardian" as const,
918
+ guardianExternalUserId: "+15550003333",
919
+ guardianChatId: "+15550003333",
783
920
  };
784
921
 
785
922
  const capturedContexts: unknown[] = [];
786
- mockStartVoiceTurn.mockImplementation(async (opts: {
787
- guardianContext?: unknown;
788
- onTextDelta: (t: string) => void;
789
- onComplete: () => void;
790
- }) => {
791
- capturedContexts.push(opts.guardianContext);
792
- opts.onTextDelta('Response.');
793
- opts.onComplete();
794
- return { turnId: `run-${capturedContexts.length}`, abort: () => {} };
795
- });
923
+ mockStartVoiceTurn.mockImplementation(
924
+ async (opts: {
925
+ guardianContext?: unknown;
926
+ onTextDelta: (t: string) => void;
927
+ onComplete: () => void;
928
+ }) => {
929
+ capturedContexts.push(opts.guardianContext);
930
+ opts.onTextDelta("Response.");
931
+ opts.onComplete();
932
+ return { turnId: `run-${capturedContexts.length}`, abort: () => {} };
933
+ },
934
+ );
796
935
 
797
- const { controller } = setupController(undefined, { guardianContext: initialCtx });
936
+ const { controller } = setupController(undefined, {
937
+ guardianContext: initialCtx,
938
+ });
798
939
 
799
940
  // First turn: unverified
800
- await controller.handleCallerUtterance('Hello');
941
+ await controller.handleCallerUtterance("Hello");
801
942
  expect(capturedContexts[0]).toEqual(initialCtx);
802
943
 
803
944
  // Simulate guardian verification succeeding
804
945
  controller.setGuardianContext(upgradedCtx);
805
946
 
806
947
  // Second turn: should use upgraded guardian context
807
- await controller.handleCallerUtterance('I verified');
948
+ await controller.handleCallerUtterance("I verified");
808
949
  expect(capturedContexts[1]).toEqual(upgradedCtx);
809
950
 
810
951
  controller.destroy();
@@ -812,7 +953,7 @@ describe('call-controller', () => {
812
953
 
813
954
  // ── destroy ───────────────────────────────────────────────────────
814
955
 
815
- test('destroy: unregisters controller', () => {
956
+ test("destroy: unregisters controller", () => {
816
957
  const { session, controller } = setupController();
817
958
 
818
959
  // Controller should be registered
@@ -824,7 +965,7 @@ describe('call-controller', () => {
824
965
  expect(getCallController(session.id)).toBeUndefined();
825
966
  });
826
967
 
827
- test('destroy: can be called multiple times without error', () => {
968
+ test("destroy: can be called multiple times without error", () => {
828
969
  const { controller } = setupController();
829
970
 
830
971
  controller.destroy();
@@ -832,27 +973,37 @@ describe('call-controller', () => {
832
973
  expect(() => controller.destroy()).not.toThrow();
833
974
  });
834
975
 
835
- test('destroy: during active turn does not trigger post-turn side effects', async () => {
976
+ test("destroy: during active turn does not trigger post-turn side effects", async () => {
836
977
  // Simulate a turn that completes after destroy() is called
837
- mockStartVoiceTurn.mockImplementation(async (opts: { signal?: AbortSignal; onTextDelta: (t: string) => void; onComplete: () => void }) => {
838
- return new Promise((resolve) => {
839
- const timeout = setTimeout(() => {
840
- opts.onTextDelta('This is a long response');
841
- opts.onComplete();
842
- resolve({ turnId: 'run-1', abort: () => {} });
843
- }, 1000);
844
-
845
- opts.signal?.addEventListener('abort', () => {
846
- clearTimeout(timeout);
847
- // The defensive abort listener in runTurn resolves turnComplete
848
- opts.onComplete();
849
- resolve({ turnId: 'run-1', abort: () => {} });
850
- }, { once: true });
851
- });
852
- });
978
+ mockStartVoiceTurn.mockImplementation(
979
+ async (opts: {
980
+ signal?: AbortSignal;
981
+ onTextDelta: (t: string) => void;
982
+ onComplete: () => void;
983
+ }) => {
984
+ return new Promise((resolve) => {
985
+ const timeout = setTimeout(() => {
986
+ opts.onTextDelta("This is a long response");
987
+ opts.onComplete();
988
+ resolve({ turnId: "run-1", abort: () => {} });
989
+ }, 1000);
990
+
991
+ opts.signal?.addEventListener(
992
+ "abort",
993
+ () => {
994
+ clearTimeout(timeout);
995
+ // The defensive abort listener in runTurn resolves turnComplete
996
+ opts.onComplete();
997
+ resolve({ turnId: "run-1", abort: () => {} });
998
+ },
999
+ { once: true },
1000
+ );
1001
+ });
1002
+ },
1003
+ );
853
1004
 
854
1005
  const { relay, controller } = setupController();
855
- const turnPromise = controller.handleCallerUtterance('Start speaking');
1006
+ const turnPromise = controller.handleCallerUtterance("Start speaking");
856
1007
 
857
1008
  // Let the turn start
858
1009
  await new Promise((r) => setTimeout(r, 5));
@@ -866,7 +1017,9 @@ describe('call-controller', () => {
866
1017
  // Verify that NO spurious post-turn side effects occurred after destroy:
867
1018
  // - No final empty-string sendTextToken('', true) call after abort
868
1019
  // The only end marker should be from handleInterrupt, not from post-turn logic
869
- const endMarkers = relay.sentTokens.filter((t) => t.token === '' && t.last === true);
1020
+ const endMarkers = relay.sentTokens.filter(
1021
+ (t) => t.token === "" && t.last === true,
1022
+ );
870
1023
 
871
1024
  // destroy() increments llmRunVersion, so isCurrentRun() returns false
872
1025
  // for the aborted turn, preventing post-turn side effects including
@@ -876,20 +1029,28 @@ describe('call-controller', () => {
876
1029
 
877
1030
  // ── handleUserInstruction ─────────────────────────────────────────
878
1031
 
879
- test('handleUserInstruction: injects instruction marker and triggers turn when idle', async () => {
880
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
881
- expect(opts.content).toContain('[USER_INSTRUCTION: Ask about their weekend plans]');
882
- const tokens = ['Sure, do you have any weekend plans?'];
883
- for (const token of tokens) {
884
- opts.onTextDelta(token);
885
- }
886
- opts.onComplete();
887
- return { turnId: 'run-instr', abort: () => {} };
888
- });
1032
+ test("handleUserInstruction: injects instruction marker and triggers turn when idle", async () => {
1033
+ mockStartVoiceTurn.mockImplementation(
1034
+ async (opts: {
1035
+ content: string;
1036
+ onTextDelta: (t: string) => void;
1037
+ onComplete: () => void;
1038
+ }) => {
1039
+ expect(opts.content).toContain(
1040
+ "[USER_INSTRUCTION: Ask about their weekend plans]",
1041
+ );
1042
+ const tokens = ["Sure, do you have any weekend plans?"];
1043
+ for (const token of tokens) {
1044
+ opts.onTextDelta(token);
1045
+ }
1046
+ opts.onComplete();
1047
+ return { turnId: "run-instr", abort: () => {} };
1048
+ },
1049
+ );
889
1050
 
890
1051
  const { relay, controller } = setupController();
891
1052
 
892
- await controller.handleUserInstruction('Ask about their weekend plans');
1053
+ await controller.handleUserInstruction("Ask about their weekend plans");
893
1054
 
894
1055
  // Should have streamed a response since controller was idle
895
1056
  const nonEmptyTokens = relay.sentTokens.filter((t) => t.token.length > 0);
@@ -898,106 +1059,125 @@ describe('call-controller', () => {
898
1059
  controller.destroy();
899
1060
  });
900
1061
 
901
- test('handleUserInstruction: emits user_instruction_relayed event', async () => {
902
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Understood, adjusting approach.']));
1062
+ test("handleUserInstruction: emits user_instruction_relayed event", async () => {
1063
+ mockStartVoiceTurn.mockImplementation(
1064
+ createMockVoiceTurn(["Understood, adjusting approach."]),
1065
+ );
903
1066
 
904
1067
  const { session, controller } = setupController();
905
1068
 
906
- await controller.handleUserInstruction('Be more formal in your tone');
1069
+ await controller.handleUserInstruction("Be more formal in your tone");
907
1070
 
908
1071
  const events = getCallEvents(session.id);
909
- const instructionEvents = events.filter((e) => e.eventType === 'user_instruction_relayed');
1072
+ const instructionEvents = events.filter(
1073
+ (e) => e.eventType === "user_instruction_relayed",
1074
+ );
910
1075
  expect(instructionEvents.length).toBe(1);
911
1076
 
912
1077
  const payload = JSON.parse(instructionEvents[0].payloadJson);
913
- expect(payload.instruction).toBe('Be more formal in your tone');
1078
+ expect(payload.instruction).toBe("Be more formal in your tone");
914
1079
 
915
1080
  controller.destroy();
916
1081
  });
917
1082
 
918
1083
  // ── Non-blocking consultation: caller follow-up during pending consultation ──
919
1084
 
920
- test('handleCallerUtterance: triggers normal turn while consultation is pending (non-blocking)', async () => {
1085
+ test("handleCallerUtterance: triggers normal turn while consultation is pending (non-blocking)", async () => {
921
1086
  // Trigger ASK_GUARDIAN to start a consultation
922
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
923
- ['Hold on. [ASK_GUARDIAN: What time works?]'],
924
- ));
1087
+ mockStartVoiceTurn.mockImplementation(
1088
+ createMockVoiceTurn(["Hold on. [ASK_GUARDIAN: What time works?]"]),
1089
+ );
925
1090
  const { controller } = setupController();
926
- await controller.handleCallerUtterance('Book me in');
1091
+ await controller.handleCallerUtterance("Book me in");
927
1092
  // Controller returns to idle; consultation tracked separately
928
- expect(controller.getState()).toBe('idle');
1093
+ expect(controller.getState()).toBe("idle");
929
1094
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
930
1095
 
931
1096
  // Track calls to startVoiceTurn from this point
932
1097
  let turnCallCount = 0;
933
- mockStartVoiceTurn.mockImplementation(async (opts: { onTextDelta: (t: string) => void; onComplete: () => void }) => {
934
- turnCallCount++;
935
- opts.onTextDelta('Sure, let me help with that.');
936
- opts.onComplete();
937
- return { turnId: 'run-followup', abort: () => {} };
938
- });
1098
+ mockStartVoiceTurn.mockImplementation(
1099
+ async (opts: {
1100
+ onTextDelta: (t: string) => void;
1101
+ onComplete: () => void;
1102
+ }) => {
1103
+ turnCallCount++;
1104
+ opts.onTextDelta("Sure, let me help with that.");
1105
+ opts.onComplete();
1106
+ return { turnId: "run-followup", abort: () => {} };
1107
+ },
1108
+ );
939
1109
 
940
1110
  // Caller speaks while consultation is pending — should trigger a normal turn
941
- await controller.handleCallerUtterance('Hello? Are you still there?');
1111
+ await controller.handleCallerUtterance("Hello? Are you still there?");
942
1112
  expect(turnCallCount).toBe(1);
943
1113
  // Controller returns to idle after the turn completes
944
- expect(controller.getState()).toBe('idle');
1114
+ expect(controller.getState()).toBe("idle");
945
1115
  // Consultation should still be pending
946
1116
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
947
1117
 
948
1118
  controller.destroy();
949
1119
  });
950
1120
 
951
- test('guardian answer arriving while controller idle: queued as instruction and flushed immediately', async () => {
1121
+ test("guardian answer arriving while controller idle: queued as instruction and flushed immediately", async () => {
952
1122
  // Trigger ASK_GUARDIAN to start a consultation
953
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
954
- ['Checking. [ASK_GUARDIAN: Confirm appointment?]'],
955
- ));
1123
+ mockStartVoiceTurn.mockImplementation(
1124
+ createMockVoiceTurn(["Checking. [ASK_GUARDIAN: Confirm appointment?]"]),
1125
+ );
956
1126
  const { controller } = setupController();
957
- await controller.handleCallerUtterance('I want to schedule');
958
- expect(controller.getState()).toBe('idle');
1127
+ await controller.handleCallerUtterance("I want to schedule");
1128
+ expect(controller.getState()).toBe("idle");
959
1129
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
960
1130
 
961
1131
  // Set up mock for the answer turn
962
1132
  const turnContents: string[] = [];
963
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
964
- turnContents.push(opts.content);
965
- opts.onTextDelta('Confirmed.');
966
- opts.onComplete();
967
- return { turnId: `run-${turnContents.length}`, abort: () => {} };
968
- });
1133
+ mockStartVoiceTurn.mockImplementation(
1134
+ async (opts: {
1135
+ content: string;
1136
+ onTextDelta: (t: string) => void;
1137
+ onComplete: () => void;
1138
+ }) => {
1139
+ turnContents.push(opts.content);
1140
+ opts.onTextDelta("Confirmed.");
1141
+ opts.onComplete();
1142
+ return { turnId: `run-${turnContents.length}`, abort: () => {} };
1143
+ },
1144
+ );
969
1145
 
970
- const accepted = await controller.handleUserAnswer('Yes, confirmed');
1146
+ const accepted = await controller.handleUserAnswer("Yes, confirmed");
971
1147
  expect(accepted).toBe(true);
972
1148
 
973
1149
  // Give fire-and-forget turns time to complete
974
1150
  await new Promise((r) => setTimeout(r, 100));
975
1151
 
976
1152
  // The answer turn should have fired with the USER_ANSWERED marker
977
- expect(turnContents.some((c) => c.includes('[USER_ANSWERED: Yes, confirmed]'))).toBe(true);
1153
+ expect(
1154
+ turnContents.some((c) => c.includes("[USER_ANSWERED: Yes, confirmed]")),
1155
+ ).toBe(true);
978
1156
  // Consultation should now be cleared
979
1157
  expect(controller.getPendingConsultationQuestionId()).toBeNull();
980
1158
 
981
1159
  controller.destroy();
982
1160
  });
983
1161
 
984
- test('no duplicate guardian dispatch: repeated informational ASK_GUARDIAN coalesces with existing consultation', async () => {
1162
+ test("no duplicate guardian dispatch: repeated informational ASK_GUARDIAN coalesces with existing consultation", async () => {
985
1163
  // Trigger ASK_GUARDIAN to start first consultation
986
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
987
- ['Let me ask. [ASK_GUARDIAN: Preferred date?]'],
988
- ));
1164
+ mockStartVoiceTurn.mockImplementation(
1165
+ createMockVoiceTurn(["Let me ask. [ASK_GUARDIAN: Preferred date?]"]),
1166
+ );
989
1167
  const { session, controller } = setupController();
990
- await controller.handleCallerUtterance('Schedule please');
991
- expect(controller.getState()).toBe('idle');
1168
+ await controller.handleCallerUtterance("Schedule please");
1169
+ expect(controller.getState()).toBe("idle");
992
1170
  const firstQuestionId = controller.getPendingConsultationQuestionId();
993
1171
  expect(firstQuestionId).not.toBeNull();
994
1172
 
995
1173
  // Model emits another informational ASK_GUARDIAN in a subsequent turn —
996
1174
  // should coalesce (same tool scope: both lack tool metadata)
997
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
998
- ['Actually let me re-check. [ASK_GUARDIAN: Preferred date again?]'],
999
- ));
1000
- await controller.handleCallerUtterance('Hello?');
1175
+ mockStartVoiceTurn.mockImplementation(
1176
+ createMockVoiceTurn([
1177
+ "Actually let me re-check. [ASK_GUARDIAN: Preferred date again?]",
1178
+ ]),
1179
+ );
1180
+ await controller.handleCallerUtterance("Hello?");
1001
1181
 
1002
1182
  // Consultation should be coalesced — same question ID retained
1003
1183
  const secondQuestionId = controller.getPendingConsultationQuestionId();
@@ -1006,64 +1186,75 @@ describe('call-controller', () => {
1006
1186
 
1007
1187
  // The session status should still be waiting_on_user
1008
1188
  const updatedSession = getCallSession(session.id);
1009
- expect(updatedSession!.status).toBe('waiting_on_user');
1189
+ expect(updatedSession!.status).toBe("waiting_on_user");
1010
1190
 
1011
1191
  controller.destroy();
1012
1192
  });
1013
1193
 
1014
- test('handleUserAnswer: returns false when no pending consultation (stale/duplicate guard)', async () => {
1194
+ test("handleUserAnswer: returns false when no pending consultation (stale/duplicate guard)", async () => {
1015
1195
  const { controller } = setupController();
1016
1196
 
1017
1197
  // No consultation pending — idle state, answer rejected
1018
- expect(await controller.handleUserAnswer('some answer')).toBe(false);
1198
+ expect(await controller.handleUserAnswer("some answer")).toBe(false);
1019
1199
 
1020
1200
  // Start a turn to enter processing state — still no consultation
1021
- mockStartVoiceTurn.mockImplementation(async (opts: { onTextDelta: (t: string) => void; onComplete: () => void }) => {
1022
- await new Promise((r) => setTimeout(r, 200));
1023
- opts.onTextDelta('Response.');
1024
- opts.onComplete();
1025
- return { turnId: 'run-proc', abort: () => {} };
1026
- });
1027
- const turnPromise = controller.handleCallerUtterance('Test');
1201
+ mockStartVoiceTurn.mockImplementation(
1202
+ async (opts: {
1203
+ onTextDelta: (t: string) => void;
1204
+ onComplete: () => void;
1205
+ }) => {
1206
+ await new Promise((r) => setTimeout(r, 200));
1207
+ opts.onTextDelta("Response.");
1208
+ opts.onComplete();
1209
+ return { turnId: "run-proc", abort: () => {} };
1210
+ },
1211
+ );
1212
+ const turnPromise = controller.handleCallerUtterance("Test");
1028
1213
  await new Promise((r) => setTimeout(r, 10));
1029
1214
  // No consultation → answer rejected regardless of controller state
1030
- expect(await controller.handleUserAnswer('stale answer')).toBe(false);
1215
+ expect(await controller.handleUserAnswer("stale answer")).toBe(false);
1031
1216
 
1032
1217
  // Clean up
1033
1218
  await turnPromise.catch(() => {});
1034
1219
  controller.destroy();
1035
1220
  });
1036
1221
 
1037
- test('duplicate answer to same consultation: first accepted, second rejected', async () => {
1222
+ test("duplicate answer to same consultation: first accepted, second rejected", async () => {
1038
1223
  // Trigger ASK_GUARDIAN consultation
1039
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1040
- ['Hold on. [ASK_GUARDIAN: What time?]'],
1041
- ));
1224
+ mockStartVoiceTurn.mockImplementation(
1225
+ createMockVoiceTurn(["Hold on. [ASK_GUARDIAN: What time?]"]),
1226
+ );
1042
1227
  const { controller } = setupController();
1043
- await controller.handleCallerUtterance('Book me');
1228
+ await controller.handleCallerUtterance("Book me");
1044
1229
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1045
1230
 
1046
1231
  // Set up mock for the answer turn
1047
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1048
- opts.onTextDelta('Got it.');
1049
- opts.onComplete();
1050
- return { turnId: 'run-answer', abort: () => {} };
1051
- });
1232
+ mockStartVoiceTurn.mockImplementation(
1233
+ async (opts: {
1234
+ content: string;
1235
+ onTextDelta: (t: string) => void;
1236
+ onComplete: () => void;
1237
+ }) => {
1238
+ opts.onTextDelta("Got it.");
1239
+ opts.onComplete();
1240
+ return { turnId: "run-answer", abort: () => {} };
1241
+ },
1242
+ );
1052
1243
 
1053
1244
  // First answer is accepted
1054
- const first = await controller.handleUserAnswer('3pm');
1245
+ const first = await controller.handleUserAnswer("3pm");
1055
1246
  expect(first).toBe(true);
1056
1247
  expect(controller.getPendingConsultationQuestionId()).toBeNull();
1057
1248
 
1058
1249
  // Second answer is rejected — consultation already consumed
1059
- const second = await controller.handleUserAnswer('4pm');
1250
+ const second = await controller.handleUserAnswer("4pm");
1060
1251
  expect(second).toBe(false);
1061
1252
 
1062
1253
  await new Promise((r) => setTimeout(r, 50));
1063
1254
  controller.destroy();
1064
1255
  });
1065
1256
 
1066
- test('handleUserInstruction: queues when processing, but triggers when idle', async () => {
1257
+ test("handleUserInstruction: queues when processing, but triggers when idle", async () => {
1067
1258
  // Track content passed to each voice turn invocation
1068
1259
  const turnContents: string[] = [];
1069
1260
  let turnCount = 0;
@@ -1071,32 +1262,40 @@ describe('call-controller', () => {
1071
1262
  // Start a slow turn to put controller in processing/speaking state.
1072
1263
  // After the first turn completes, the mock switches to a fast handler
1073
1264
  // that captures content so we can verify the flushed instruction.
1074
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1075
- turnCount++;
1076
- if (turnCount === 1) {
1077
- // First turn: slow, simulates processing state
1078
- await new Promise((r) => setTimeout(r, 100));
1079
- opts.onTextDelta('Response.');
1265
+ mockStartVoiceTurn.mockImplementation(
1266
+ async (opts: {
1267
+ content: string;
1268
+ onTextDelta: (t: string) => void;
1269
+ onComplete: () => void;
1270
+ }) => {
1271
+ turnCount++;
1272
+ if (turnCount === 1) {
1273
+ // First turn: slow, simulates processing state
1274
+ await new Promise((r) => setTimeout(r, 100));
1275
+ opts.onTextDelta("Response.");
1276
+ opts.onComplete();
1277
+ return { turnId: "run-1", abort: () => {} };
1278
+ }
1279
+ // Subsequent turns: capture content and complete immediately
1280
+ turnContents.push(opts.content);
1281
+ opts.onTextDelta("Noted.");
1080
1282
  opts.onComplete();
1081
- return { turnId: 'run-1', abort: () => {} };
1082
- }
1083
- // Subsequent turns: capture content and complete immediately
1084
- turnContents.push(opts.content);
1085
- opts.onTextDelta('Noted.');
1086
- opts.onComplete();
1087
- return { turnId: `run-${turnCount}`, abort: () => {} };
1088
- });
1283
+ return { turnId: `run-${turnCount}`, abort: () => {} };
1284
+ },
1285
+ );
1089
1286
 
1090
1287
  const { session, controller } = setupController();
1091
- const turnPromise = controller.handleCallerUtterance('Hello');
1288
+ const turnPromise = controller.handleCallerUtterance("Hello");
1092
1289
  await new Promise((r) => setTimeout(r, 10));
1093
1290
 
1094
1291
  // Inject instruction while processing — should be queued
1095
- await controller.handleUserInstruction('Suggest morning slots');
1292
+ await controller.handleUserInstruction("Suggest morning slots");
1096
1293
 
1097
1294
  // Event should be recorded even when queued
1098
1295
  const events = getCallEvents(session.id);
1099
- const instructionEvents = events.filter((e) => e.eventType === 'user_instruction_relayed');
1296
+ const instructionEvents = events.filter(
1297
+ (e) => e.eventType === "user_instruction_relayed",
1298
+ );
1100
1299
  expect(instructionEvents.length).toBe(1);
1101
1300
 
1102
1301
  // Wait for the first turn to finish (instructions flushed at turn boundary)
@@ -1107,47 +1306,61 @@ describe('call-controller', () => {
1107
1306
 
1108
1307
  // The queued instruction should have been flushed into a new turn
1109
1308
  expect(turnContents.length).toBeGreaterThanOrEqual(1);
1110
- expect(turnContents.some((c) => c.includes('[USER_INSTRUCTION: Suggest morning slots]'))).toBe(true);
1309
+ expect(
1310
+ turnContents.some((c) =>
1311
+ c.includes("[USER_INSTRUCTION: Suggest morning slots]"),
1312
+ ),
1313
+ ).toBe(true);
1111
1314
 
1112
1315
  // Controller should return to idle after the flush turn completes
1113
- expect(controller.getState()).toBe('idle');
1316
+ expect(controller.getState()).toBe("idle");
1114
1317
 
1115
1318
  controller.destroy();
1116
1319
  });
1117
1320
 
1118
1321
  // ── Post-end-call drain guard ───────────────────────────────────
1119
1322
 
1120
- test('handleUserAnswer: answer turn ends call with END_CALL, no further turns after completion', async () => {
1323
+ test("handleUserAnswer: answer turn ends call with END_CALL, no further turns after completion", async () => {
1121
1324
  // Trigger ASK_GUARDIAN to start a consultation
1122
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1123
- ['Checking. [ASK_GUARDIAN: Confirm cancellation?]'],
1124
- ));
1325
+ mockStartVoiceTurn.mockImplementation(
1326
+ createMockVoiceTurn(["Checking. [ASK_GUARDIAN: Confirm cancellation?]"]),
1327
+ );
1125
1328
  const { session, relay, controller } = setupController();
1126
- await controller.handleCallerUtterance('I want to cancel');
1127
- expect(controller.getState()).toBe('idle');
1329
+ await controller.handleCallerUtterance("I want to cancel");
1330
+ expect(controller.getState()).toBe("idle");
1128
1331
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1129
1332
 
1130
1333
  // Set up mock so the answer turn ends the call with [END_CALL]
1131
1334
  const turnContents: string[] = [];
1132
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1133
- turnContents.push(opts.content);
1134
- opts.onTextDelta('Alright, your appointment is cancelled. Goodbye! [END_CALL]');
1135
- opts.onComplete();
1136
- return { turnId: `run-${turnContents.length}`, abort: () => {} };
1137
- });
1335
+ mockStartVoiceTurn.mockImplementation(
1336
+ async (opts: {
1337
+ content: string;
1338
+ onTextDelta: (t: string) => void;
1339
+ onComplete: () => void;
1340
+ }) => {
1341
+ turnContents.push(opts.content);
1342
+ opts.onTextDelta(
1343
+ "Alright, your appointment is cancelled. Goodbye! [END_CALL]",
1344
+ );
1345
+ opts.onComplete();
1346
+ return { turnId: `run-${turnContents.length}`, abort: () => {} };
1347
+ },
1348
+ );
1138
1349
 
1139
- const accepted = await controller.handleUserAnswer('Yes, cancel it');
1350
+ const accepted = await controller.handleUserAnswer("Yes, cancel it");
1140
1351
  expect(accepted).toBe(true);
1141
1352
 
1142
1353
  // Give fire-and-forget turns time to complete
1143
1354
  await new Promise((r) => setTimeout(r, 100));
1144
1355
 
1145
1356
  // The answer turn should have fired
1146
- expect(turnContents.some((c) => c.includes('[USER_ANSWERED: Yes, cancel it]'))).toBe(true);
1357
+ expect(
1358
+ turnContents.some((c) => c.includes("[USER_ANSWERED: Yes, cancel it]")),
1359
+ ).toBe(true);
1147
1360
 
1148
1361
  // Call should be completed
1149
1362
  const updatedSession = getCallSession(session.id);
1150
- expect(updatedSession!.status).toBe('completed');
1363
+ expect(updatedSession!.status).toBe("completed");
1151
1364
  expect(relay.endCalled).toBe(true);
1152
1365
 
1153
1366
  controller.destroy();
@@ -1155,27 +1368,35 @@ describe('call-controller', () => {
1155
1368
 
1156
1369
  // ── Consultation timeout with generated turn ─────────────────────
1157
1370
 
1158
- test('consultation timeout: fires generated turn with GUARDIAN_TIMEOUT instruction instead of hardcoded text', async () => {
1371
+ test("consultation timeout: fires generated turn with GUARDIAN_TIMEOUT instruction instead of hardcoded text", async () => {
1159
1372
  // Use a short consultation timeout so we can wait for it in the test
1160
1373
  mockConsultationTimeoutMs = 50;
1161
1374
 
1162
1375
  // Trigger ASK_GUARDIAN to start a consultation
1163
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1164
- ['Let me check. [ASK_GUARDIAN: What time works?]'],
1165
- ));
1376
+ mockStartVoiceTurn.mockImplementation(
1377
+ createMockVoiceTurn(["Let me check. [ASK_GUARDIAN: What time works?]"]),
1378
+ );
1166
1379
  const { session, relay, controller } = setupController();
1167
- await controller.handleCallerUtterance('Book me in');
1168
- expect(controller.getState()).toBe('idle');
1380
+ await controller.handleCallerUtterance("Book me in");
1381
+ expect(controller.getState()).toBe("idle");
1169
1382
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1170
1383
 
1171
1384
  // Set up mock to capture what content the timeout turn receives
1172
1385
  const turnContents: string[] = [];
1173
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1174
- turnContents.push(opts.content);
1175
- opts.onTextDelta('I\'m sorry, I wasn\'t able to reach them. Would you like a callback?');
1176
- opts.onComplete();
1177
- return { turnId: `run-${turnContents.length}`, abort: () => {} };
1178
- });
1386
+ mockStartVoiceTurn.mockImplementation(
1387
+ async (opts: {
1388
+ content: string;
1389
+ onTextDelta: (t: string) => void;
1390
+ onComplete: () => void;
1391
+ }) => {
1392
+ turnContents.push(opts.content);
1393
+ opts.onTextDelta(
1394
+ "I'm sorry, I wasn't able to reach them. Would you like a callback?",
1395
+ );
1396
+ opts.onComplete();
1397
+ return { turnId: `run-${turnContents.length}`, abort: () => {} };
1398
+ },
1399
+ );
1179
1400
 
1180
1401
  // Wait for the short consultation timeout to fire
1181
1402
  await new Promise((r) => setTimeout(r, 200));
@@ -1184,50 +1405,60 @@ describe('call-controller', () => {
1184
1405
  // The instruction starts with '[' so flushPendingInstructions passes it through
1185
1406
  // without wrapping it in [USER_INSTRUCTION:].
1186
1407
  expect(turnContents.length).toBe(1);
1187
- expect(turnContents[0]).toContain('[GUARDIAN_TIMEOUT]');
1188
- expect(turnContents[0]).toContain('What time works?');
1408
+ expect(turnContents[0]).toContain("[GUARDIAN_TIMEOUT]");
1409
+ expect(turnContents[0]).toContain("What time works?");
1189
1410
 
1190
1411
  // No hardcoded timeout text should appear in relay tokens
1191
- const allText = relay.sentTokens.map((t) => t.token).join('');
1192
- expect(allText).not.toContain('I\'m sorry, I wasn\'t able to get that information in time');
1193
- expect(allText).toContain('callback');
1412
+ const allText = relay.sentTokens.map((t) => t.token).join("");
1413
+ expect(allText).not.toContain(
1414
+ "I'm sorry, I wasn't able to get that information in time",
1415
+ );
1416
+ expect(allText).toContain("callback");
1194
1417
 
1195
1418
  // Session should be back in progress
1196
1419
  const updatedSession = getCallSession(session.id);
1197
- expect(updatedSession!.status).toBe('in_progress');
1420
+ expect(updatedSession!.status).toBe("in_progress");
1198
1421
 
1199
1422
  controller.destroy();
1200
1423
  });
1201
1424
 
1202
- test('consultation timeout: timeout instruction fires even when controller is idle', async () => {
1425
+ test("consultation timeout: timeout instruction fires even when controller is idle", async () => {
1203
1426
  // Use a short consultation timeout so we can wait for it in the test
1204
1427
  mockConsultationTimeoutMs = 50;
1205
1428
 
1206
1429
  // Trigger ASK_GUARDIAN to start a consultation
1207
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1208
- ['Let me check. [ASK_GUARDIAN: What time works?]'],
1209
- ));
1430
+ mockStartVoiceTurn.mockImplementation(
1431
+ createMockVoiceTurn(["Let me check. [ASK_GUARDIAN: What time works?]"]),
1432
+ );
1210
1433
  const { controller } = setupController();
1211
- await controller.handleCallerUtterance('Book me in');
1212
- expect(controller.getState()).toBe('idle');
1434
+ await controller.handleCallerUtterance("Book me in");
1435
+ expect(controller.getState()).toBe("idle");
1213
1436
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1214
1437
 
1215
1438
  // Set up mock to capture what content the timeout turn receives
1216
1439
  const turnContents: string[] = [];
1217
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1218
- turnContents.push(opts.content);
1219
- opts.onTextDelta('Got it, I was unable to reach them.');
1220
- opts.onComplete();
1221
- return { turnId: `run-${turnContents.length}`, abort: () => {} };
1222
- });
1440
+ mockStartVoiceTurn.mockImplementation(
1441
+ async (opts: {
1442
+ content: string;
1443
+ onTextDelta: (t: string) => void;
1444
+ onComplete: () => void;
1445
+ }) => {
1446
+ turnContents.push(opts.content);
1447
+ opts.onTextDelta("Got it, I was unable to reach them.");
1448
+ opts.onComplete();
1449
+ return { turnId: `run-${turnContents.length}`, abort: () => {} };
1450
+ },
1451
+ );
1223
1452
 
1224
1453
  // Wait for the short consultation timeout to fire
1225
1454
  await new Promise((r) => setTimeout(r, 200));
1226
1455
 
1227
1456
  // The timeout instruction turn should have fired
1228
- const timeoutTurns = turnContents.filter((c) => c.includes('[GUARDIAN_TIMEOUT]'));
1457
+ const timeoutTurns = turnContents.filter((c) =>
1458
+ c.includes("[GUARDIAN_TIMEOUT]"),
1459
+ );
1229
1460
  expect(timeoutTurns.length).toBe(1);
1230
- expect(timeoutTurns[0]).toContain('What time works?');
1461
+ expect(timeoutTurns[0]).toContain("What time works?");
1231
1462
 
1232
1463
  // Consultation should be cleared after timeout
1233
1464
  expect(controller.getPendingConsultationQuestionId()).toBeNull();
@@ -1235,30 +1466,34 @@ describe('call-controller', () => {
1235
1466
  controller.destroy();
1236
1467
  });
1237
1468
 
1238
- test('consultation timeout: marks linked guardian action request as timed out', async () => {
1469
+ test("consultation timeout: marks linked guardian action request as timed out", async () => {
1239
1470
  mockConsultationTimeoutMs = 50;
1240
1471
 
1241
1472
  // Trigger ASK_GUARDIAN to start a consultation
1242
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1243
- ['Let me check. [ASK_GUARDIAN: What time works?]'],
1244
- ));
1473
+ mockStartVoiceTurn.mockImplementation(
1474
+ createMockVoiceTurn(["Let me check. [ASK_GUARDIAN: What time works?]"]),
1475
+ );
1245
1476
  const { session, controller } = setupController();
1246
- await controller.handleCallerUtterance('Book me in');
1247
- expect(controller.getState()).toBe('idle');
1477
+ await controller.handleCallerUtterance("Book me in");
1478
+ expect(controller.getState()).toBe("idle");
1248
1479
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1249
1480
 
1250
1481
  // Give the async dispatchGuardianQuestion a tick to create the request
1251
1482
  await new Promise((r) => setTimeout(r, 10));
1252
1483
 
1253
1484
  // Verify a guardian action request was created
1254
- const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1485
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(
1486
+ session.id,
1487
+ );
1255
1488
  expect(pendingRequest).not.toBeNull();
1256
- expect(pendingRequest!.status).toBe('pending');
1489
+ expect(pendingRequest!.status).toBe("pending");
1257
1490
 
1258
1491
  // Set up mock for the timeout-generated turn
1259
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1260
- ['I\'m sorry, I couldn\'t reach them. Would you like a callback?'],
1261
- ));
1492
+ mockStartVoiceTurn.mockImplementation(
1493
+ createMockVoiceTurn([
1494
+ "I'm sorry, I couldn't reach them. Would you like a callback?",
1495
+ ]),
1496
+ );
1262
1497
 
1263
1498
  // Wait for the consultation timeout
1264
1499
  await new Promise((r) => setTimeout(r, 200));
@@ -1266,11 +1501,13 @@ describe('call-controller', () => {
1266
1501
  // The canonical guardian request should now be expired
1267
1502
  const timedOutRequest = getCanonicalGuardianRequest(pendingRequest!.id);
1268
1503
  expect(timedOutRequest).not.toBeNull();
1269
- expect(timedOutRequest!.status).toBe('expired');
1504
+ expect(timedOutRequest!.status).toBe("expired");
1270
1505
 
1271
1506
  // Event should be recorded
1272
1507
  const events = getCallEvents(session.id);
1273
- const timeoutEvents = events.filter((e) => e.eventType === 'guardian_consultation_timed_out');
1508
+ const timeoutEvents = events.filter(
1509
+ (e) => e.eventType === "guardian_consultation_timed_out",
1510
+ );
1274
1511
  expect(timeoutEvents.length).toBe(1);
1275
1512
 
1276
1513
  controller.destroy();
@@ -1278,59 +1515,75 @@ describe('call-controller', () => {
1278
1515
 
1279
1516
  // ── Guardian unavailable skip after timeout ────────────────────────
1280
1517
 
1281
- test('ASK_GUARDIAN after timeout: skips wait and injects GUARDIAN_UNAVAILABLE instruction', async () => {
1518
+ test("ASK_GUARDIAN after timeout: skips wait and injects GUARDIAN_UNAVAILABLE instruction", async () => {
1282
1519
  mockConsultationTimeoutMs = 50;
1283
1520
 
1284
1521
  // Step 1: Trigger ASK_GUARDIAN to start a consultation
1285
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1286
- ['Let me check. [ASK_GUARDIAN: What time works?]'],
1287
- ));
1522
+ mockStartVoiceTurn.mockImplementation(
1523
+ createMockVoiceTurn(["Let me check. [ASK_GUARDIAN: What time works?]"]),
1524
+ );
1288
1525
  const { session, controller } = setupController();
1289
- await controller.handleCallerUtterance('Book me in');
1290
- expect(controller.getState()).toBe('idle');
1526
+ await controller.handleCallerUtterance("Book me in");
1527
+ expect(controller.getState()).toBe("idle");
1291
1528
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1292
1529
 
1293
1530
  // Step 2: Set up mock for timeout-generated turn
1294
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1295
- ['I\'m sorry, I couldn\'t reach them. Would you like a callback?'],
1296
- ));
1531
+ mockStartVoiceTurn.mockImplementation(
1532
+ createMockVoiceTurn([
1533
+ "I'm sorry, I couldn't reach them. Would you like a callback?",
1534
+ ]),
1535
+ );
1297
1536
 
1298
1537
  // Wait for the consultation timeout
1299
1538
  await new Promise((r) => setTimeout(r, 200));
1300
- expect(controller.getState()).toBe('idle');
1539
+ expect(controller.getState()).toBe("idle");
1301
1540
 
1302
1541
  // Step 3: Model tries ASK_GUARDIAN again in a subsequent turn
1303
1542
  const turnContents: string[] = [];
1304
1543
  let turnCount = 0;
1305
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1306
- turnCount++;
1307
- turnContents.push(opts.content);
1308
- if (turnCount === 1) {
1309
- // First turn: model emits ASK_GUARDIAN again
1310
- opts.onTextDelta('Let me check on that. [ASK_GUARDIAN: What about 3pm?]');
1311
- } else {
1312
- // Second turn: model should handle the GUARDIAN_UNAVAILABLE instruction
1313
- opts.onTextDelta('Unfortunately I can\'t reach them. Anything else I can help with?');
1314
- }
1315
- opts.onComplete();
1316
- return { turnId: `run-${turnCount}`, abort: () => {} };
1317
- });
1544
+ mockStartVoiceTurn.mockImplementation(
1545
+ async (opts: {
1546
+ content: string;
1547
+ onTextDelta: (t: string) => void;
1548
+ onComplete: () => void;
1549
+ }) => {
1550
+ turnCount++;
1551
+ turnContents.push(opts.content);
1552
+ if (turnCount === 1) {
1553
+ // First turn: model emits ASK_GUARDIAN again
1554
+ opts.onTextDelta(
1555
+ "Let me check on that. [ASK_GUARDIAN: What about 3pm?]",
1556
+ );
1557
+ } else {
1558
+ // Second turn: model should handle the GUARDIAN_UNAVAILABLE instruction
1559
+ opts.onTextDelta(
1560
+ "Unfortunately I can't reach them. Anything else I can help with?",
1561
+ );
1562
+ }
1563
+ opts.onComplete();
1564
+ return { turnId: `run-${turnCount}`, abort: () => {} };
1565
+ },
1566
+ );
1318
1567
 
1319
- await controller.handleCallerUtterance('Can we try another time?');
1568
+ await controller.handleCallerUtterance("Can we try another time?");
1320
1569
 
1321
1570
  // Give the queued instruction flush time to fire
1322
1571
  await new Promise((r) => setTimeout(r, 100));
1323
1572
 
1324
1573
  // The second turn should contain the GUARDIAN_UNAVAILABLE instruction
1325
1574
  expect(turnCount).toBeGreaterThanOrEqual(2);
1326
- expect(turnContents.some((c) => c.includes('[GUARDIAN_UNAVAILABLE]'))).toBe(true);
1575
+ expect(turnContents.some((c) => c.includes("[GUARDIAN_UNAVAILABLE]"))).toBe(
1576
+ true,
1577
+ );
1327
1578
  // Controller remains idle; no new consultation created
1328
- expect(controller.getState()).toBe('idle');
1579
+ expect(controller.getState()).toBe("idle");
1329
1580
  expect(controller.getPendingConsultationQuestionId()).toBeNull();
1330
1581
 
1331
1582
  // The skip should be recorded as an event
1332
1583
  const events = getCallEvents(session.id);
1333
- const skipEvents = events.filter((e) => e.eventType === 'guardian_unavailable_skipped');
1584
+ const skipEvents = events.filter(
1585
+ (e) => e.eventType === "guardian_unavailable_skipped",
1586
+ );
1334
1587
  expect(skipEvents.length).toBe(1);
1335
1588
 
1336
1589
  controller.destroy();
@@ -1338,169 +1591,193 @@ describe('call-controller', () => {
1338
1591
 
1339
1592
  // ── Structured tool-approval ASK_GUARDIAN_APPROVAL ──────────────────
1340
1593
 
1341
- test('ASK_GUARDIAN_APPROVAL: persists toolName and inputDigest on guardian action request', async () => {
1594
+ test("ASK_GUARDIAN_APPROVAL: persists toolName and inputDigest on guardian action request", async () => {
1342
1595
  const approvalPayload = JSON.stringify({
1343
- question: 'Allow send_email to bob@example.com?',
1344
- toolName: 'send_email',
1345
- input: { to: 'bob@example.com', subject: 'Hello' },
1596
+ question: "Allow send_email to bob@example.com?",
1597
+ toolName: "send_email",
1598
+ input: { to: "bob@example.com", subject: "Hello" },
1346
1599
  });
1347
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1348
- [`Let me check with your guardian. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`],
1349
- ));
1350
- const { session, relay, controller } = setupController('Send an email');
1600
+ mockStartVoiceTurn.mockImplementation(
1601
+ createMockVoiceTurn([
1602
+ `Let me check with your guardian. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
1603
+ ]),
1604
+ );
1605
+ const { session, relay, controller } = setupController("Send an email");
1351
1606
 
1352
- await controller.handleCallerUtterance('Send an email to Bob');
1607
+ await controller.handleCallerUtterance("Send an email to Bob");
1353
1608
 
1354
1609
  // Give the async dispatchGuardianQuestion a tick to create the request
1355
1610
  await new Promise((r) => setTimeout(r, 50));
1356
1611
 
1357
1612
  // Controller returns to idle (non-blocking); consultation tracked separately
1358
- expect(controller.getState()).toBe('idle');
1613
+ expect(controller.getState()).toBe("idle");
1359
1614
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1360
1615
 
1361
1616
  // Verify a pending question was created with the correct text
1362
1617
  const question = getPendingQuestion(session.id);
1363
1618
  expect(question).not.toBeNull();
1364
- expect(question!.questionText).toBe('Allow send_email to bob@example.com?');
1619
+ expect(question!.questionText).toBe("Allow send_email to bob@example.com?");
1365
1620
 
1366
1621
  // Verify the guardian action request has tool metadata
1367
- const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1622
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(
1623
+ session.id,
1624
+ );
1368
1625
  expect(pendingRequest).not.toBeNull();
1369
- expect(pendingRequest!.toolName).toBe('send_email');
1626
+ expect(pendingRequest!.toolName).toBe("send_email");
1370
1627
  expect(pendingRequest!.inputDigest).not.toBeNull();
1371
1628
  expect(pendingRequest!.inputDigest!.length).toBe(64); // SHA-256 hex = 64 chars
1372
1629
 
1373
1630
  // The ASK_GUARDIAN_APPROVAL marker should NOT appear in the relay tokens
1374
- const allText = relay.sentTokens.map((t) => t.token).join('');
1375
- expect(allText).not.toContain('[ASK_GUARDIAN_APPROVAL:');
1376
- expect(allText).not.toContain('send_email');
1631
+ const allText = relay.sentTokens.map((t) => t.token).join("");
1632
+ expect(allText).not.toContain("[ASK_GUARDIAN_APPROVAL:");
1633
+ expect(allText).not.toContain("send_email");
1377
1634
 
1378
1635
  controller.destroy();
1379
1636
  });
1380
1637
 
1381
- test('ASK_GUARDIAN_APPROVAL: computes deterministic digest for same tool+input', async () => {
1638
+ test("ASK_GUARDIAN_APPROVAL: computes deterministic digest for same tool+input", async () => {
1382
1639
  const approvalPayload = JSON.stringify({
1383
- question: 'Allow send_email?',
1384
- toolName: 'send_email',
1385
- input: { subject: 'Hello', to: 'bob@example.com' },
1640
+ question: "Allow send_email?",
1641
+ toolName: "send_email",
1642
+ input: { subject: "Hello", to: "bob@example.com" },
1386
1643
  });
1387
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1388
- [`Checking. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`],
1389
- ));
1390
- const { session, controller } = setupController('Send email');
1644
+ mockStartVoiceTurn.mockImplementation(
1645
+ createMockVoiceTurn([
1646
+ `Checking. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
1647
+ ]),
1648
+ );
1649
+ const { session, controller } = setupController("Send email");
1391
1650
 
1392
- await controller.handleCallerUtterance('Send it');
1651
+ await controller.handleCallerUtterance("Send it");
1393
1652
  await new Promise((r) => setTimeout(r, 50));
1394
1653
 
1395
1654
  const request1 = getPendingCanonicalRequestByCallSessionId(session.id);
1396
1655
  expect(request1).not.toBeNull();
1397
1656
 
1398
1657
  // Compute expected digest independently using the same utility
1399
- const { computeToolApprovalDigest } = await import('../security/tool-approval-digest.js');
1400
- const expectedDigest = computeToolApprovalDigest('send_email', { subject: 'Hello', to: 'bob@example.com' });
1658
+ const { computeToolApprovalDigest } =
1659
+ await import("../security/tool-approval-digest.js");
1660
+ const expectedDigest = computeToolApprovalDigest("send_email", {
1661
+ subject: "Hello",
1662
+ to: "bob@example.com",
1663
+ });
1401
1664
  expect(request1!.inputDigest).toBe(expectedDigest);
1402
1665
 
1403
1666
  controller.destroy();
1404
1667
  });
1405
1668
 
1406
- test('informational ASK_GUARDIAN: does NOT persist tool metadata (null toolName/inputDigest)', async () => {
1407
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1408
- ['Let me check. [ASK_GUARDIAN: What date works best?]'],
1409
- ));
1410
- const { session, controller } = setupController('Book appointment');
1669
+ test("informational ASK_GUARDIAN: does NOT persist tool metadata (null toolName/inputDigest)", async () => {
1670
+ mockStartVoiceTurn.mockImplementation(
1671
+ createMockVoiceTurn([
1672
+ "Let me check. [ASK_GUARDIAN: What date works best?]",
1673
+ ]),
1674
+ );
1675
+ const { session, controller } = setupController("Book appointment");
1411
1676
 
1412
- await controller.handleCallerUtterance('I need to schedule something');
1677
+ await controller.handleCallerUtterance("I need to schedule something");
1413
1678
  await new Promise((r) => setTimeout(r, 50));
1414
1679
 
1415
1680
  // Verify the guardian action request has NO tool metadata
1416
- const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1681
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(
1682
+ session.id,
1683
+ );
1417
1684
  expect(pendingRequest).not.toBeNull();
1418
1685
  expect(pendingRequest!.toolName).toBeNull();
1419
1686
  expect(pendingRequest!.inputDigest).toBeNull();
1420
- expect(pendingRequest!.questionText).toBe('What date works best?');
1687
+ expect(pendingRequest!.questionText).toBe("What date works best?");
1421
1688
 
1422
1689
  controller.destroy();
1423
1690
  });
1424
1691
 
1425
- test('ASK_GUARDIAN_APPROVAL: strips marker from TTS output', async () => {
1692
+ test("ASK_GUARDIAN_APPROVAL: strips marker from TTS output", async () => {
1426
1693
  const approvalPayload = JSON.stringify({
1427
- question: 'Allow calendar_create?',
1428
- toolName: 'calendar_create',
1429
- input: { date: '2026-03-01', title: 'Meeting' },
1694
+ question: "Allow calendar_create?",
1695
+ toolName: "calendar_create",
1696
+ input: { date: "2026-03-01", title: "Meeting" },
1430
1697
  });
1431
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn([
1432
- 'Let me get approval for that. ',
1433
- `[ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
1434
- ' Thank you.',
1435
- ]));
1436
- const { relay, controller } = setupController('Create event');
1698
+ mockStartVoiceTurn.mockImplementation(
1699
+ createMockVoiceTurn([
1700
+ "Let me get approval for that. ",
1701
+ `[ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
1702
+ " Thank you.",
1703
+ ]),
1704
+ );
1705
+ const { relay, controller } = setupController("Create event");
1437
1706
 
1438
- await controller.handleCallerUtterance('Create a meeting');
1707
+ await controller.handleCallerUtterance("Create a meeting");
1439
1708
 
1440
- const allText = relay.sentTokens.map((t) => t.token).join('');
1441
- expect(allText).toContain('Let me get approval');
1442
- expect(allText).not.toContain('[ASK_GUARDIAN_APPROVAL:');
1443
- expect(allText).not.toContain('calendar_create');
1444
- expect(allText).not.toContain('inputDigest');
1709
+ const allText = relay.sentTokens.map((t) => t.token).join("");
1710
+ expect(allText).toContain("Let me get approval");
1711
+ expect(allText).not.toContain("[ASK_GUARDIAN_APPROVAL:");
1712
+ expect(allText).not.toContain("calendar_create");
1713
+ expect(allText).not.toContain("inputDigest");
1445
1714
 
1446
1715
  controller.destroy();
1447
1716
  });
1448
1717
 
1449
- test('ASK_GUARDIAN_APPROVAL: handles JSON payloads containing }] in string values', async () => {
1718
+ test("ASK_GUARDIAN_APPROVAL: handles JSON payloads containing }] in string values", async () => {
1450
1719
  // The `}]` sequence inside a JSON string value previously caused the
1451
1720
  // non-greedy regex to terminate early, truncating the JSON and leaking
1452
1721
  // partial data into TTS output.
1453
1722
  const approvalPayload = JSON.stringify({
1454
- question: 'Allow send_message?',
1455
- toolName: 'send_message',
1456
- input: { msg: 'test}]more', nested: { key: 'value with }] braces' } },
1723
+ question: "Allow send_message?",
1724
+ toolName: "send_message",
1725
+ input: { msg: "test}]more", nested: { key: "value with }] braces" } },
1457
1726
  });
1458
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1459
- [`Let me check. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`],
1460
- ));
1461
- const { session, relay, controller } = setupController('Send a message');
1727
+ mockStartVoiceTurn.mockImplementation(
1728
+ createMockVoiceTurn([
1729
+ `Let me check. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
1730
+ ]),
1731
+ );
1732
+ const { session, relay, controller } = setupController("Send a message");
1462
1733
 
1463
- await controller.handleCallerUtterance('Send it');
1734
+ await controller.handleCallerUtterance("Send it");
1464
1735
  await new Promise((r) => setTimeout(r, 50));
1465
1736
 
1466
1737
  // Controller returns to idle (non-blocking); consultation tracked separately
1467
- expect(controller.getState()).toBe('idle');
1738
+ expect(controller.getState()).toBe("idle");
1468
1739
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1469
1740
  const question = getPendingQuestion(session.id);
1470
1741
  expect(question).not.toBeNull();
1471
- expect(question!.questionText).toBe('Allow send_message?');
1742
+ expect(question!.questionText).toBe("Allow send_message?");
1472
1743
 
1473
1744
  // Verify tool metadata was parsed correctly
1474
- const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1745
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(
1746
+ session.id,
1747
+ );
1475
1748
  expect(pendingRequest).not.toBeNull();
1476
- expect(pendingRequest!.toolName).toBe('send_message');
1749
+ expect(pendingRequest!.toolName).toBe("send_message");
1477
1750
  expect(pendingRequest!.inputDigest).not.toBeNull();
1478
1751
 
1479
1752
  // No partial JSON or marker text should leak into TTS output
1480
- const allText = relay.sentTokens.map((t) => t.token).join('');
1481
- expect(allText).not.toContain('[ASK_GUARDIAN_APPROVAL:');
1482
- expect(allText).not.toContain('send_message');
1483
- expect(allText).not.toContain('}]');
1484
- expect(allText).not.toContain('test}]more');
1485
- expect(allText).toContain('Let me check.');
1753
+ const allText = relay.sentTokens.map((t) => t.token).join("");
1754
+ expect(allText).not.toContain("[ASK_GUARDIAN_APPROVAL:");
1755
+ expect(allText).not.toContain("send_message");
1756
+ expect(allText).not.toContain("}]");
1757
+ expect(allText).not.toContain("test}]more");
1758
+ expect(allText).toContain("Let me check.");
1486
1759
 
1487
1760
  controller.destroy();
1488
1761
  });
1489
1762
 
1490
- test('ASK_GUARDIAN_APPROVAL with malformed JSON: falls through to informational ASK_GUARDIAN', async () => {
1763
+ test("ASK_GUARDIAN_APPROVAL with malformed JSON: falls through to informational ASK_GUARDIAN", async () => {
1491
1764
  // Malformed JSON in the approval marker — should be ignored, and if there's
1492
1765
  // also an informational ASK_GUARDIAN marker, it should be used instead
1493
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1494
- ['Checking. [ASK_GUARDIAN_APPROVAL: {invalid json}] [ASK_GUARDIAN: Fallback question?]'],
1495
- ));
1496
- const { session, controller } = setupController('Test fallback');
1766
+ mockStartVoiceTurn.mockImplementation(
1767
+ createMockVoiceTurn([
1768
+ "Checking. [ASK_GUARDIAN_APPROVAL: {invalid json}] [ASK_GUARDIAN: Fallback question?]",
1769
+ ]),
1770
+ );
1771
+ const { session, controller } = setupController("Test fallback");
1497
1772
 
1498
- await controller.handleCallerUtterance('Do something');
1773
+ await controller.handleCallerUtterance("Do something");
1499
1774
  await new Promise((r) => setTimeout(r, 50));
1500
1775
 
1501
- const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1776
+ const pendingRequest = getPendingCanonicalRequestByCallSessionId(
1777
+ session.id,
1778
+ );
1502
1779
  expect(pendingRequest).not.toBeNull();
1503
- expect(pendingRequest!.questionText).toBe('Fallback question?');
1780
+ expect(pendingRequest!.questionText).toBe("Fallback question?");
1504
1781
  // Tool metadata should be null since the approval marker was malformed
1505
1782
  expect(pendingRequest!.toolName).toBeNull();
1506
1783
  expect(pendingRequest!.inputDigest).toBeNull();
@@ -1510,37 +1787,47 @@ describe('call-controller', () => {
1510
1787
 
1511
1788
  // ── Non-blocking race safety ───────────────────────────────────────
1512
1789
 
1513
- test('guardian answer during processing/speaking: queued in pendingInstructions and applied at next turn boundary', async () => {
1790
+ test("guardian answer during processing/speaking: queued in pendingInstructions and applied at next turn boundary", async () => {
1514
1791
  // Trigger ASK_GUARDIAN to start a consultation
1515
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1516
- ['Checking. [ASK_GUARDIAN: Confirm appointment?]'],
1517
- ));
1792
+ mockStartVoiceTurn.mockImplementation(
1793
+ createMockVoiceTurn(["Checking. [ASK_GUARDIAN: Confirm appointment?]"]),
1794
+ );
1518
1795
  const { controller } = setupController();
1519
- await controller.handleCallerUtterance('I want to schedule');
1520
- expect(controller.getState()).toBe('idle');
1796
+ await controller.handleCallerUtterance("I want to schedule");
1797
+ expect(controller.getState()).toBe("idle");
1521
1798
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1522
1799
 
1523
1800
  // Start a new turn (caller follow-up) to put controller in processing state
1524
1801
  let firstTurnResolve: (() => void) | null = null;
1525
1802
  const turnContents: string[] = [];
1526
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1527
- turnContents.push(opts.content);
1528
- if (!firstTurnResolve) {
1529
- // First turn: pause to simulate processing state
1530
- await new Promise<void>((resolve) => { firstTurnResolve = resolve; });
1531
- }
1532
- opts.onTextDelta('Response.');
1533
- opts.onComplete();
1534
- return { turnId: `run-${turnContents.length}`, abort: () => {} };
1535
- });
1803
+ mockStartVoiceTurn.mockImplementation(
1804
+ async (opts: {
1805
+ content: string;
1806
+ onTextDelta: (t: string) => void;
1807
+ onComplete: () => void;
1808
+ }) => {
1809
+ turnContents.push(opts.content);
1810
+ if (!firstTurnResolve) {
1811
+ // First turn: pause to simulate processing state
1812
+ await new Promise<void>((resolve) => {
1813
+ firstTurnResolve = resolve;
1814
+ });
1815
+ }
1816
+ opts.onTextDelta("Response.");
1817
+ opts.onComplete();
1818
+ return { turnId: `run-${turnContents.length}`, abort: () => {} };
1819
+ },
1820
+ );
1536
1821
 
1537
1822
  // Start a caller turn that will pause mid-processing
1538
- const callerTurnPromise = controller.handleCallerUtterance('Are you still there?');
1823
+ const callerTurnPromise = controller.handleCallerUtterance(
1824
+ "Are you still there?",
1825
+ );
1539
1826
  await new Promise((r) => setTimeout(r, 10));
1540
- expect(controller.getState()).toBe('speaking');
1827
+ expect(controller.getState()).toBe("speaking");
1541
1828
 
1542
1829
  // Answer arrives while the controller is processing/speaking
1543
- const accepted = await controller.handleUserAnswer('3pm works');
1830
+ const accepted = await controller.handleUserAnswer("3pm works");
1544
1831
  expect(accepted).toBe(true);
1545
1832
  // Consultation is consumed immediately
1546
1833
  expect(controller.getPendingConsultationQuestionId()).toBeNull();
@@ -1553,26 +1840,28 @@ describe('call-controller', () => {
1553
1840
  await new Promise((r) => setTimeout(r, 100));
1554
1841
 
1555
1842
  // The queued USER_ANSWERED instruction should have been applied
1556
- expect(turnContents.some((c) => c.includes('[USER_ANSWERED: 3pm works]'))).toBe(true);
1843
+ expect(
1844
+ turnContents.some((c) => c.includes("[USER_ANSWERED: 3pm works]")),
1845
+ ).toBe(true);
1557
1846
 
1558
1847
  controller.destroy();
1559
1848
  });
1560
1849
 
1561
- test('timeout + late answer: after timeout, a late answer is rejected as stale', async () => {
1850
+ test("timeout + late answer: after timeout, a late answer is rejected as stale", async () => {
1562
1851
  mockConsultationTimeoutMs = 50;
1563
1852
 
1564
1853
  // Trigger ASK_GUARDIAN to start a consultation
1565
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1566
- ['Let me check. [ASK_GUARDIAN: What time works?]'],
1567
- ));
1854
+ mockStartVoiceTurn.mockImplementation(
1855
+ createMockVoiceTurn(["Let me check. [ASK_GUARDIAN: What time works?]"]),
1856
+ );
1568
1857
  const { controller } = setupController();
1569
- await controller.handleCallerUtterance('Book me in');
1858
+ await controller.handleCallerUtterance("Book me in");
1570
1859
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1571
1860
 
1572
1861
  // Set up mock for the timeout-generated turn
1573
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1574
- ['Sorry, I could not reach them.'],
1575
- ));
1862
+ mockStartVoiceTurn.mockImplementation(
1863
+ createMockVoiceTurn(["Sorry, I could not reach them."]),
1864
+ );
1576
1865
 
1577
1866
  // Wait for the consultation timeout to expire the consultation
1578
1867
  await new Promise((r) => setTimeout(r, 200));
@@ -1581,55 +1870,61 @@ describe('call-controller', () => {
1581
1870
  expect(controller.getPendingConsultationQuestionId()).toBeNull();
1582
1871
 
1583
1872
  // A late answer should be rejected
1584
- const lateResult = await controller.handleUserAnswer('3pm is fine');
1873
+ const lateResult = await controller.handleUserAnswer("3pm is fine");
1585
1874
  expect(lateResult).toBe(false);
1586
1875
 
1587
1876
  controller.destroy();
1588
1877
  });
1589
1878
 
1590
- test('caller follow-up processed normally while consultation pending', async () => {
1879
+ test("caller follow-up processed normally while consultation pending", async () => {
1591
1880
  // Trigger ASK_GUARDIAN to start a consultation
1592
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1593
- ['Let me check. [ASK_GUARDIAN: What date?]'],
1594
- ));
1881
+ mockStartVoiceTurn.mockImplementation(
1882
+ createMockVoiceTurn(["Let me check. [ASK_GUARDIAN: What date?]"]),
1883
+ );
1595
1884
  const { relay, controller } = setupController();
1596
- await controller.handleCallerUtterance('Schedule something');
1885
+ await controller.handleCallerUtterance("Schedule something");
1597
1886
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1598
1887
 
1599
1888
  // Caller follows up while consultation is pending
1600
1889
  const turnContents: string[] = [];
1601
- mockStartVoiceTurn.mockImplementation(async (opts: { content: string; onTextDelta: (t: string) => void; onComplete: () => void }) => {
1602
- turnContents.push(opts.content);
1603
- opts.onTextDelta('Of course, what else can I help with?');
1604
- opts.onComplete();
1605
- return { turnId: `run-${turnContents.length}`, abort: () => {} };
1606
- });
1890
+ mockStartVoiceTurn.mockImplementation(
1891
+ async (opts: {
1892
+ content: string;
1893
+ onTextDelta: (t: string) => void;
1894
+ onComplete: () => void;
1895
+ }) => {
1896
+ turnContents.push(opts.content);
1897
+ opts.onTextDelta("Of course, what else can I help with?");
1898
+ opts.onComplete();
1899
+ return { turnId: `run-${turnContents.length}`, abort: () => {} };
1900
+ },
1901
+ );
1607
1902
 
1608
- await controller.handleCallerUtterance('Can you also check availability?');
1903
+ await controller.handleCallerUtterance("Can you also check availability?");
1609
1904
 
1610
1905
  // The follow-up should trigger a normal turn (non-blocking)
1611
1906
  expect(turnContents.length).toBe(1);
1612
- expect(turnContents[0]).toContain('Can you also check availability?');
1907
+ expect(turnContents[0]).toContain("Can you also check availability?");
1613
1908
 
1614
1909
  // Consultation should still be pending
1615
1910
  expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
1616
1911
 
1617
1912
  // Response should appear in relay
1618
- const allText = relay.sentTokens.map((t) => t.token).join('');
1619
- expect(allText).toContain('what else can I help with');
1913
+ const allText = relay.sentTokens.map((t) => t.token).join("");
1914
+ expect(allText).toContain("what else can I help with");
1620
1915
 
1621
1916
  controller.destroy();
1622
1917
  });
1623
1918
 
1624
1919
  // ── Consultation coalescing (Incident C) ────────────────────────────
1625
1920
 
1626
- test('coalescing: repeated identical informational ASK_GUARDIAN does not create a new request', async () => {
1921
+ test("coalescing: repeated identical informational ASK_GUARDIAN does not create a new request", async () => {
1627
1922
  // Trigger first ASK_GUARDIAN
1628
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1629
- ['Let me ask. [ASK_GUARDIAN: Preferred date?]'],
1630
- ));
1923
+ mockStartVoiceTurn.mockImplementation(
1924
+ createMockVoiceTurn(["Let me ask. [ASK_GUARDIAN: Preferred date?]"]),
1925
+ );
1631
1926
  const { session, controller } = setupController();
1632
- await controller.handleCallerUtterance('Schedule please');
1927
+ await controller.handleCallerUtterance("Schedule please");
1633
1928
  await new Promise((r) => setTimeout(r, 50));
1634
1929
 
1635
1930
  const firstQuestionId = controller.getPendingConsultationQuestionId();
@@ -1638,40 +1933,46 @@ describe('call-controller', () => {
1638
1933
  expect(firstRequest).not.toBeNull();
1639
1934
 
1640
1935
  // Repeated ASK_GUARDIAN with same informational question (no tool metadata)
1641
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1642
- ['Still checking. [ASK_GUARDIAN: Preferred date?]'],
1643
- ));
1644
- await controller.handleCallerUtterance('Hello? Still there?');
1936
+ mockStartVoiceTurn.mockImplementation(
1937
+ createMockVoiceTurn(["Still checking. [ASK_GUARDIAN: Preferred date?]"]),
1938
+ );
1939
+ await controller.handleCallerUtterance("Hello? Still there?");
1645
1940
  await new Promise((r) => setTimeout(r, 50));
1646
1941
 
1647
1942
  // Should coalesce: same consultation ID, same request
1648
1943
  expect(controller.getPendingConsultationQuestionId()).toBe(firstQuestionId);
1649
- const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1944
+ const currentRequest = getPendingCanonicalRequestByCallSessionId(
1945
+ session.id,
1946
+ );
1650
1947
  expect(currentRequest).not.toBeNull();
1651
1948
  expect(currentRequest!.id).toBe(firstRequest!.id);
1652
- expect(currentRequest!.status).toBe('pending');
1949
+ expect(currentRequest!.status).toBe("pending");
1653
1950
 
1654
1951
  // Coalesce event should be recorded
1655
1952
  const events = getCallEvents(session.id);
1656
- const coalesceEvents = events.filter((e) => e.eventType === 'guardian_consult_coalesced');
1953
+ const coalesceEvents = events.filter(
1954
+ (e) => e.eventType === "guardian_consult_coalesced",
1955
+ );
1657
1956
  expect(coalesceEvents.length).toBe(1);
1658
1957
 
1659
1958
  controller.destroy();
1660
1959
  });
1661
1960
 
1662
- test('coalescing: repeated ASK_GUARDIAN_APPROVAL with same tool/input does not create a new request', async () => {
1961
+ test("coalescing: repeated ASK_GUARDIAN_APPROVAL with same tool/input does not create a new request", async () => {
1663
1962
  const approvalPayload = JSON.stringify({
1664
- question: 'Allow send_email to bob@example.com?',
1665
- toolName: 'send_email',
1666
- input: { to: 'bob@example.com', subject: 'Hello' },
1963
+ question: "Allow send_email to bob@example.com?",
1964
+ toolName: "send_email",
1965
+ input: { to: "bob@example.com", subject: "Hello" },
1667
1966
  });
1668
1967
 
1669
1968
  // First ASK_GUARDIAN_APPROVAL
1670
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1671
- [`Checking. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`],
1672
- ));
1673
- const { session, controller } = setupController('Send email');
1674
- await controller.handleCallerUtterance('Send email to Bob');
1969
+ mockStartVoiceTurn.mockImplementation(
1970
+ createMockVoiceTurn([
1971
+ `Checking. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
1972
+ ]),
1973
+ );
1974
+ const { session, controller } = setupController("Send email");
1975
+ await controller.handleCallerUtterance("Send email to Bob");
1675
1976
  await new Promise((r) => setTimeout(r, 50));
1676
1977
 
1677
1978
  const firstQuestionId = controller.getPendingConsultationQuestionId();
@@ -1680,102 +1981,120 @@ describe('call-controller', () => {
1680
1981
  expect(firstRequest).not.toBeNull();
1681
1982
 
1682
1983
  // Repeated ASK_GUARDIAN_APPROVAL with same tool/input
1683
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1684
- [`Still checking. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`],
1685
- ));
1686
- await controller.handleCallerUtterance('Can you send it already?');
1984
+ mockStartVoiceTurn.mockImplementation(
1985
+ createMockVoiceTurn([
1986
+ `Still checking. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
1987
+ ]),
1988
+ );
1989
+ await controller.handleCallerUtterance("Can you send it already?");
1687
1990
  await new Promise((r) => setTimeout(r, 50));
1688
1991
 
1689
1992
  // Should coalesce: same consultation, same request
1690
1993
  expect(controller.getPendingConsultationQuestionId()).toBe(firstQuestionId);
1691
- const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1994
+ const currentRequest = getPendingCanonicalRequestByCallSessionId(
1995
+ session.id,
1996
+ );
1692
1997
  expect(currentRequest!.id).toBe(firstRequest!.id);
1693
- expect(currentRequest!.status).toBe('pending');
1998
+ expect(currentRequest!.status).toBe("pending");
1694
1999
 
1695
2000
  controller.destroy();
1696
2001
  });
1697
2002
 
1698
- test('supersession: materially different tool triggers new request with superseded metadata', async () => {
2003
+ test("supersession: materially different tool triggers new request with superseded metadata", async () => {
1699
2004
  const firstPayload = JSON.stringify({
1700
- question: 'Allow send_email?',
1701
- toolName: 'send_email',
1702
- input: { to: 'bob@example.com' },
2005
+ question: "Allow send_email?",
2006
+ toolName: "send_email",
2007
+ input: { to: "bob@example.com" },
1703
2008
  });
1704
2009
 
1705
2010
  // First ASK_GUARDIAN_APPROVAL for send_email
1706
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1707
- [`Checking. [ASK_GUARDIAN_APPROVAL: ${firstPayload}]`],
1708
- ));
1709
- const { session, controller } = setupController('Process request');
1710
- await controller.handleCallerUtterance('Send email');
2011
+ mockStartVoiceTurn.mockImplementation(
2012
+ createMockVoiceTurn([
2013
+ `Checking. [ASK_GUARDIAN_APPROVAL: ${firstPayload}]`,
2014
+ ]),
2015
+ );
2016
+ const { session, controller } = setupController("Process request");
2017
+ await controller.handleCallerUtterance("Send email");
1711
2018
  await new Promise((r) => setTimeout(r, 50));
1712
2019
 
1713
2020
  const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1714
2021
  expect(firstRequest).not.toBeNull();
1715
- expect(firstRequest!.toolName).toBe('send_email');
2022
+ expect(firstRequest!.toolName).toBe("send_email");
1716
2023
 
1717
2024
  // Different tool — should supersede
1718
2025
  const secondPayload = JSON.stringify({
1719
- question: 'Allow calendar_create?',
1720
- toolName: 'calendar_create',
1721
- input: { date: '2026-03-01' },
2026
+ question: "Allow calendar_create?",
2027
+ toolName: "calendar_create",
2028
+ input: { date: "2026-03-01" },
1722
2029
  });
1723
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1724
- [`Actually, let me do this. [ASK_GUARDIAN_APPROVAL: ${secondPayload}]`],
1725
- ));
1726
- await controller.handleCallerUtterance('Actually, create a calendar event instead');
2030
+ mockStartVoiceTurn.mockImplementation(
2031
+ createMockVoiceTurn([
2032
+ `Actually, let me do this. [ASK_GUARDIAN_APPROVAL: ${secondPayload}]`,
2033
+ ]),
2034
+ );
2035
+ await controller.handleCallerUtterance(
2036
+ "Actually, create a calendar event instead",
2037
+ );
1727
2038
  await new Promise((r) => setTimeout(r, 100));
1728
2039
 
1729
2040
  // New consultation should be active
1730
2041
  const secondRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1731
2042
  expect(secondRequest).not.toBeNull();
1732
2043
  expect(secondRequest!.id).not.toBe(firstRequest!.id);
1733
- expect(secondRequest!.toolName).toBe('calendar_create');
2044
+ expect(secondRequest!.toolName).toBe("calendar_create");
1734
2045
 
1735
2046
  // Old request should be expired (superseded by the new one)
1736
2047
  const expiredRequest = getCanonicalGuardianRequest(firstRequest!.id);
1737
2048
  expect(expiredRequest).not.toBeNull();
1738
- expect(expiredRequest!.status).toBe('expired');
2049
+ expect(expiredRequest!.status).toBe("expired");
1739
2050
 
1740
2051
  controller.destroy();
1741
2052
  });
1742
2053
 
1743
- test('tool metadata continuity: re-ask without structured metadata inherits tool scope from prior consultation', async () => {
2054
+ test("tool metadata continuity: re-ask without structured metadata inherits tool scope from prior consultation", async () => {
1744
2055
  const approvalPayload = JSON.stringify({
1745
- question: 'Allow send_email?',
1746
- toolName: 'send_email',
1747
- input: { to: 'bob@example.com', subject: 'Hello' },
2056
+ question: "Allow send_email?",
2057
+ toolName: "send_email",
2058
+ input: { to: "bob@example.com", subject: "Hello" },
1748
2059
  });
1749
2060
 
1750
2061
  // First ask with structured tool metadata
1751
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1752
- [`Let me check. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`],
1753
- ));
1754
- const { session, controller } = setupController('Send email');
1755
- await controller.handleCallerUtterance('Send email to Bob');
2062
+ mockStartVoiceTurn.mockImplementation(
2063
+ createMockVoiceTurn([
2064
+ `Let me check. [ASK_GUARDIAN_APPROVAL: ${approvalPayload}]`,
2065
+ ]),
2066
+ );
2067
+ const { session, controller } = setupController("Send email");
2068
+ await controller.handleCallerUtterance("Send email to Bob");
1756
2069
  await new Promise((r) => setTimeout(r, 50));
1757
2070
 
1758
2071
  const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
1759
2072
  expect(firstRequest).not.toBeNull();
1760
- expect(firstRequest!.toolName).toBe('send_email');
2073
+ expect(firstRequest!.toolName).toBe("send_email");
1761
2074
 
1762
2075
  // Re-ask with informational ASK_GUARDIAN (no structured metadata).
1763
2076
  // Since the tool metadata matches the existing consultation (inherited),
1764
2077
  // this should coalesce rather than supersede.
1765
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(
1766
- ['Checking again. [ASK_GUARDIAN: Can I send that email?]'],
1767
- ));
1768
- await controller.handleCallerUtterance('Can you hurry up?');
2078
+ mockStartVoiceTurn.mockImplementation(
2079
+ createMockVoiceTurn([
2080
+ "Checking again. [ASK_GUARDIAN: Can I send that email?]",
2081
+ ]),
2082
+ );
2083
+ await controller.handleCallerUtterance("Can you hurry up?");
1769
2084
  await new Promise((r) => setTimeout(r, 50));
1770
2085
 
1771
2086
  // Should coalesce: the inherited tool metadata matches the existing consultation
1772
- const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
2087
+ const currentRequest = getPendingCanonicalRequestByCallSessionId(
2088
+ session.id,
2089
+ );
1773
2090
  expect(currentRequest!.id).toBe(firstRequest!.id);
1774
- expect(currentRequest!.status).toBe('pending');
2091
+ expect(currentRequest!.status).toBe("pending");
1775
2092
 
1776
2093
  // Coalesce event should be recorded
1777
2094
  const events = getCallEvents(session.id);
1778
- const coalesceEvents = events.filter((e) => e.eventType === 'guardian_consult_coalesced');
2095
+ const coalesceEvents = events.filter(
2096
+ (e) => e.eventType === "guardian_consult_coalesced",
2097
+ );
1779
2098
  expect(coalesceEvents.length).toBe(1);
1780
2099
 
1781
2100
  controller.destroy();
@@ -1788,21 +2107,21 @@ describe('call-controller', () => {
1788
2107
  const { relay, controller } = setupController();
1789
2108
 
1790
2109
  // Simulate guardian wait state on the relay
1791
- relay.mockConnectionState = 'awaiting_guardian_decision';
2110
+ relay.mockConnectionState = "awaiting_guardian_decision";
1792
2111
 
1793
2112
  // Wait for the silence timeout to fire
1794
2113
  await new Promise((r) => setTimeout(r, 200));
1795
2114
 
1796
2115
  // "Are you still there?" should NOT have been sent
1797
2116
  const silenceTokens = relay.sentTokens.filter((t) =>
1798
- t.token.includes('Are you still there?'),
2117
+ t.token.includes("Are you still there?"),
1799
2118
  );
1800
2119
  expect(silenceTokens.length).toBe(0);
1801
2120
 
1802
2121
  controller.destroy();
1803
2122
  });
1804
2123
 
1805
- test('silence timeout fires normally when not in guardian wait', async () => {
2124
+ test("silence timeout fires normally when not in guardian wait", async () => {
1806
2125
  mockSilenceTimeoutMs = 50; // Short timeout for testing
1807
2126
  const { relay, controller } = setupController();
1808
2127
 
@@ -1813,7 +2132,7 @@ describe('call-controller', () => {
1813
2132
 
1814
2133
  // "Are you still there?" SHOULD have been sent
1815
2134
  const silenceTokens = relay.sentTokens.filter((t) =>
1816
- t.token.includes('Are you still there?'),
2135
+ t.token.includes("Are you still there?"),
1817
2136
  );
1818
2137
  expect(silenceTokens.length).toBe(1);
1819
2138
 
@@ -1822,23 +2141,25 @@ describe('call-controller', () => {
1822
2141
 
1823
2142
  // ── Pointer message regression tests ─────────────────────────────
1824
2143
 
1825
- test('END_CALL marker writes completed pointer to origin conversation', async () => {
1826
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Goodbye! [END_CALL]']));
2144
+ test("END_CALL marker writes completed pointer to origin conversation", async () => {
2145
+ mockStartVoiceTurn.mockImplementation(
2146
+ createMockVoiceTurn(["Goodbye! [END_CALL]"]),
2147
+ );
1827
2148
  const { controller } = setupControllerWithOrigin();
1828
2149
 
1829
- await controller.handleCallerUtterance('Bye');
2150
+ await controller.handleCallerUtterance("Bye");
1830
2151
  // Allow async pointer write to flush
1831
2152
  await new Promise((r) => setTimeout(r, 100));
1832
2153
 
1833
- const text = getLatestAssistantText('conv-ctrl-origin');
2154
+ const text = getLatestAssistantText("conv-ctrl-origin");
1834
2155
  expect(text).not.toBeNull();
1835
- expect(text!).toContain('+15552222222');
1836
- expect(text!).toContain('completed');
2156
+ expect(text!).toContain("+15552222222");
2157
+ expect(text!).toContain("completed");
1837
2158
 
1838
2159
  controller.destroy();
1839
2160
  });
1840
2161
 
1841
- test('max duration timeout writes completed pointer to origin conversation', async () => {
2162
+ test("max duration timeout writes completed pointer to origin conversation", async () => {
1842
2163
  // Use a very short max duration to trigger the timeout quickly.
1843
2164
  // The real MAX_CALL_DURATION_MS mock is 12 minutes; override via
1844
2165
  // call-constants mock (already set to 12*60*1000). Instead, we
@@ -1848,16 +2169,18 @@ describe('call-controller', () => {
1848
2169
  // For this test, we check that when the session has an
1849
2170
  // initiatedFromConversationId and startedAt, the completion pointer
1850
2171
  // is written with a duration.
1851
- mockStartVoiceTurn.mockImplementation(createMockVoiceTurn(['Goodbye! [END_CALL]']));
2172
+ mockStartVoiceTurn.mockImplementation(
2173
+ createMockVoiceTurn(["Goodbye! [END_CALL]"]),
2174
+ );
1852
2175
  const { controller } = setupControllerWithOrigin();
1853
2176
 
1854
- await controller.handleCallerUtterance('End call');
2177
+ await controller.handleCallerUtterance("End call");
1855
2178
  await new Promise((r) => setTimeout(r, 100));
1856
2179
 
1857
- const text = getLatestAssistantText('conv-ctrl-origin');
2180
+ const text = getLatestAssistantText("conv-ctrl-origin");
1858
2181
  expect(text).not.toBeNull();
1859
- expect(text!).toContain('+15552222222');
1860
- expect(text!).toContain('completed');
2182
+ expect(text!).toContain("+15552222222");
2183
+ expect(text!).toContain("completed");
1861
2184
 
1862
2185
  controller.destroy();
1863
2186
  });