@vellumai/assistant 0.3.13 → 0.3.15

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 (57) hide show
  1. package/ARCHITECTURE.md +17 -3
  2. package/Dockerfile +1 -1
  3. package/README.md +2 -0
  4. package/docs/architecture/scheduling.md +81 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +22 -0
  7. package/src/__tests__/channel-policy.test.ts +19 -0
  8. package/src/__tests__/guardian-control-plane-policy.test.ts +582 -0
  9. package/src/__tests__/guardian-outbound-http.test.ts +8 -8
  10. package/src/__tests__/intent-routing.test.ts +22 -0
  11. package/src/__tests__/ipc-snapshot.test.ts +10 -0
  12. package/src/__tests__/notification-routing-intent.test.ts +185 -0
  13. package/src/__tests__/recording-handler.test.ts +191 -31
  14. package/src/__tests__/recording-intent-fallback.test.ts +180 -0
  15. package/src/__tests__/recording-intent-handler.test.ts +597 -74
  16. package/src/__tests__/recording-intent.test.ts +738 -342
  17. package/src/__tests__/recording-state-machine.test.ts +1109 -0
  18. package/src/__tests__/reminder-store.test.ts +20 -18
  19. package/src/__tests__/reminder.test.ts +2 -1
  20. package/src/channels/config.ts +1 -1
  21. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -11
  22. package/src/config/bundled-skills/screen-recording/SKILL.md +91 -12
  23. package/src/config/system-prompt.ts +5 -0
  24. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  25. package/src/daemon/handlers/config-channels.ts +6 -6
  26. package/src/daemon/handlers/index.ts +1 -1
  27. package/src/daemon/handlers/misc.ts +258 -102
  28. package/src/daemon/handlers/recording.ts +417 -5
  29. package/src/daemon/handlers/sessions.ts +142 -68
  30. package/src/daemon/ipc-contract/computer-use.ts +23 -3
  31. package/src/daemon/ipc-contract/messages.ts +3 -1
  32. package/src/daemon/ipc-contract/shared.ts +6 -0
  33. package/src/daemon/ipc-contract-inventory.json +2 -0
  34. package/src/daemon/lifecycle.ts +2 -0
  35. package/src/daemon/recording-executor.ts +180 -0
  36. package/src/daemon/recording-intent-fallback.ts +132 -0
  37. package/src/daemon/recording-intent.ts +306 -15
  38. package/src/daemon/session-tool-setup.ts +4 -0
  39. package/src/memory/conversation-attention-store.ts +5 -5
  40. package/src/notifications/README.md +69 -1
  41. package/src/notifications/adapters/sms.ts +80 -0
  42. package/src/notifications/broadcaster.ts +1 -0
  43. package/src/notifications/copy-composer.ts +3 -3
  44. package/src/notifications/decision-engine.ts +70 -1
  45. package/src/notifications/decisions-store.ts +24 -0
  46. package/src/notifications/destination-resolver.ts +2 -1
  47. package/src/notifications/emit-signal.ts +35 -3
  48. package/src/notifications/signal.ts +6 -0
  49. package/src/notifications/types.ts +3 -0
  50. package/src/runtime/guardian-outbound-actions.ts +9 -9
  51. package/src/runtime/http-server.ts +7 -7
  52. package/src/runtime/routes/conversation-attention-routes.ts +3 -3
  53. package/src/runtime/routes/integration-routes.ts +5 -5
  54. package/src/schedule/scheduler.ts +15 -3
  55. package/src/tools/executor.ts +29 -0
  56. package/src/tools/guardian-control-plane-policy.ts +141 -0
  57. package/src/tools/types.ts +2 -0
@@ -1,389 +1,785 @@
1
- import { describe, expect,test } from 'bun:test';
1
+ import { beforeEach,describe, expect, mock, test } from 'bun:test';
2
2
 
3
3
  import {
4
- classifyRecordingIntent,
5
- detectRecordingIntent,
6
- detectStopRecordingIntent,
7
- isInterrogative,
8
- isRecordingOnly,
9
- isStopRecordingOnly,
10
- stripRecordingIntent,
11
- stripStopRecordingIntent,
4
+ resolveRecordingIntent,
12
5
  } from '../daemon/recording-intent.js';
13
6
 
14
- // ─── detectRecordingIntent ──────────────────────────────────────────────────
15
-
16
- describe('detectRecordingIntent', () => {
17
- test.each([
18
- 'record my screen',
19
- 'Record My Screen',
20
- 'record the screen',
21
- 'screen recording',
22
- 'screen record',
23
- 'start recording',
24
- 'begin recording',
25
- 'capture my screen',
26
- 'capture my display',
27
- 'capture screen',
28
- 'make a recording',
29
- 'make a screen recording',
30
- ])('detects recording intent in "%s"', (text) => {
31
- expect(detectRecordingIntent(text)).toBe(true);
32
- });
7
+ // ─── resolveRecordingIntent ─────────────────────────────────────────────────
8
+
9
+ describe('resolveRecordingIntent', () => {
10
+ // ── Start detection (covers legacy detectRecordingIntent behavior) ───────
11
+
12
+ describe('start intent detection', () => {
13
+ test.each([
14
+ 'record my screen',
15
+ 'Record My Screen',
16
+ 'record the screen',
17
+ 'screen recording',
18
+ 'screen record',
19
+ 'start recording',
20
+ 'begin recording',
21
+ 'capture my screen',
22
+ 'capture my display',
23
+ 'capture screen',
24
+ 'make a recording',
25
+ ])('detects start intent in "%s"', (text) => {
26
+ const result = resolveRecordingIntent(text);
27
+ expect(result.kind).toBe('start_only');
28
+ });
29
+
30
+ // "make a screen recording" is detected as recording intent but resolves
31
+ // to start_with_remainder because the strip patterns match "screen recording"
32
+ // first, leaving "make a" as residual text.
33
+ test('detects start intent in "make a screen recording" (with residual remainder)', () => {
34
+ const result = resolveRecordingIntent('make a screen recording');
35
+ expect(result.kind).toBe('start_with_remainder');
36
+ });
37
+
38
+ test.each([
39
+ '',
40
+ 'hello world',
41
+ 'open Safari',
42
+ 'take a screenshot',
43
+ 'what time is it?',
44
+ 'record a note',
45
+ 'make a note',
46
+ 'start the timer',
47
+ ])('does not detect start intent in "%s"', (text) => {
48
+ expect(resolveRecordingIntent(text)).toEqual({ kind: 'none' });
49
+ });
50
+
51
+ test('is case-insensitive', () => {
52
+ expect(resolveRecordingIntent('RECORD MY SCREEN').kind).toBe('start_only');
53
+ expect(resolveRecordingIntent('Screen Recording').kind).toBe('start_only');
54
+ expect(resolveRecordingIntent('START RECORDING').kind).toBe('start_only');
55
+ });
56
+ });
57
+
58
+ // ── Pure start (covers legacy isRecordingOnly behavior) ─────────────────
59
+
60
+ describe('pure start (recording-only)', () => {
61
+ test.each([
62
+ 'record my screen',
63
+ 'Record my screen',
64
+ 'start recording',
65
+ 'screen recording',
66
+ 'begin recording',
67
+ 'capture my screen',
68
+ 'make a recording',
69
+ ])('resolves as start_only for pure recording request "%s"', (text) => {
70
+ expect(resolveRecordingIntent(text)).toEqual({ kind: 'start_only' });
71
+ });
72
+
73
+ test('resolves as start_only when polite fillers surround the recording request', () => {
74
+ expect(resolveRecordingIntent('please record my screen')).toEqual({ kind: 'start_only' });
75
+ expect(resolveRecordingIntent('can you start recording')).toEqual({ kind: 'start_only' });
76
+ expect(resolveRecordingIntent('could you record my screen please')).toEqual({ kind: 'start_only' });
77
+ expect(resolveRecordingIntent('hey, start recording now')).toEqual({ kind: 'start_only' });
78
+ expect(resolveRecordingIntent('just record my screen, thanks')).toEqual({ kind: 'start_only' });
79
+ expect(resolveRecordingIntent('can you start recording?')).toEqual({ kind: 'start_only' });
80
+ });
81
+
82
+ test.each([
83
+ 'record my screen and then open Safari',
84
+ 'do this task and record my screen',
85
+ 'record my screen while I work on the document',
86
+ 'open Chrome and start recording',
87
+ 'record my screen and send it to Bob',
88
+ ])('resolves as start_with_remainder for mixed-intent "%s"', (text) => {
89
+ expect(resolveRecordingIntent(text).kind).toBe('start_with_remainder');
90
+ });
91
+
92
+ test('handles punctuation in recording-only prompts', () => {
93
+ expect(resolveRecordingIntent('record my screen!')).toEqual({ kind: 'start_only' });
94
+ expect(resolveRecordingIntent('start recording.')).toEqual({ kind: 'start_only' });
95
+ expect(resolveRecordingIntent('screen recording?')).toEqual({ kind: 'start_only' });
96
+ });
97
+ });
98
+
99
+ // ── Stop detection (covers legacy detectStopRecordingIntent behavior) ───
100
+
101
+ describe('stop intent detection', () => {
102
+ test.each([
103
+ 'stop recording',
104
+ 'stop the recording',
105
+ 'end recording',
106
+ 'end the recording',
107
+ 'finish recording',
108
+ 'finish the recording',
109
+ 'halt recording',
110
+ 'halt the recording',
111
+ ])('detects stop intent in "%s"', (text) => {
112
+ expect(resolveRecordingIntent(text).kind).toBe('stop_only');
113
+ });
114
+
115
+ test.each([
116
+ '',
117
+ 'hello world',
118
+ 'stop it',
119
+ 'end it',
120
+ 'quit',
121
+ 'take a screenshot',
122
+ 'stop the music',
123
+ ])('does not detect stop intent in "%s"', (text) => {
124
+ expect(resolveRecordingIntent(text)).toEqual({ kind: 'none' });
125
+ });
126
+
127
+ test('is case-insensitive', () => {
128
+ expect(resolveRecordingIntent('STOP RECORDING').kind).toBe('stop_only');
129
+ expect(resolveRecordingIntent('Stop The Recording').kind).toBe('stop_only');
130
+ expect(resolveRecordingIntent('END RECORDING').kind).toBe('stop_only');
131
+ });
132
+ });
133
+
134
+ // ── Pure stop (covers legacy isStopRecordingOnly behavior) ──────────────
135
+
136
+ describe('pure stop (stop-recording-only)', () => {
137
+ test.each([
138
+ 'stop recording',
139
+ 'stop the recording',
140
+ 'end recording',
141
+ 'end the recording',
142
+ 'finish recording',
143
+ 'halt recording',
144
+ ])('resolves as stop_only for pure stop request "%s"', (text) => {
145
+ expect(resolveRecordingIntent(text)).toEqual({ kind: 'stop_only' });
146
+ });
147
+
148
+ test('resolves as stop_only when polite fillers surround the stop request', () => {
149
+ expect(resolveRecordingIntent('please stop recording')).toEqual({ kind: 'stop_only' });
150
+ expect(resolveRecordingIntent('can you stop the recording?')).toEqual({ kind: 'stop_only' });
151
+ expect(resolveRecordingIntent('could you end the recording please')).toEqual({ kind: 'stop_only' });
152
+ expect(resolveRecordingIntent('stop the recording now')).toEqual({ kind: 'stop_only' });
153
+ expect(resolveRecordingIntent('just stop recording, thanks')).toEqual({ kind: 'stop_only' });
154
+ });
155
+
156
+ test('resolves as stop_with_remainder when stop has additional task', () => {
157
+ const r1 = resolveRecordingIntent('stop recording and open Chrome');
158
+ expect(r1.kind).toBe('stop_with_remainder');
159
+ if (r1.kind === 'stop_with_remainder') {
160
+ expect(r1.remainder).toContain('open Chrome');
161
+ }
162
+ });
163
+
164
+ test('handles ambiguous phrases as none', () => {
165
+ expect(resolveRecordingIntent('end it')).toEqual({ kind: 'none' });
166
+ expect(resolveRecordingIntent('stop')).toEqual({ kind: 'none' });
167
+ expect(resolveRecordingIntent('quit')).toEqual({ kind: 'none' });
168
+ });
169
+
170
+ test('handles punctuation', () => {
171
+ expect(resolveRecordingIntent('stop recording!')).toEqual({ kind: 'stop_only' });
172
+ expect(resolveRecordingIntent('stop recording.')).toEqual({ kind: 'stop_only' });
173
+ expect(resolveRecordingIntent('end the recording?')).toEqual({ kind: 'stop_only' });
174
+ });
175
+ });
176
+
177
+ // ── Remainder extraction (covers legacy strip* behavior) ────────────────
178
+
179
+ describe('remainder extraction', () => {
180
+ test('extracts remainder when start intent is mixed with other task', () => {
181
+ const r1 = resolveRecordingIntent('open Safari and record my screen');
182
+ expect(r1.kind).toBe('start_with_remainder');
183
+ if (r1.kind === 'start_with_remainder') {
184
+ expect(r1.remainder).toBe('open Safari');
185
+ }
186
+
187
+ const r2 = resolveRecordingIntent('do this task and start recording');
188
+ expect(r2.kind).toBe('start_with_remainder');
189
+ if (r2.kind === 'start_with_remainder') {
190
+ expect(r2.remainder).toContain('do this task');
191
+ }
192
+ });
193
+
194
+ test('extracts remainder when stop intent is mixed with other task', () => {
195
+ const r1 = resolveRecordingIntent('open Chrome and stop recording');
196
+ expect(r1.kind).toBe('stop_with_remainder');
197
+ if (r1.kind === 'stop_with_remainder') {
198
+ expect(r1.remainder).toBe('open Chrome');
199
+ }
200
+
201
+ const r2 = resolveRecordingIntent('save the file and end the recording');
202
+ expect(r2.kind).toBe('stop_with_remainder');
203
+ if (r2.kind === 'stop_with_remainder') {
204
+ expect(r2.remainder).toContain('save the file');
205
+ }
206
+ });
207
+
208
+ test('remainder does not contain double spaces', () => {
209
+ const r1 = resolveRecordingIntent('open Safari and also record my screen please');
210
+ if (r1.kind === 'start_with_remainder') {
211
+ expect(r1.remainder).not.toContain(' ');
212
+ }
213
+
214
+ const r2 = resolveRecordingIntent('open Safari and also stop recording please');
215
+ if (r2.kind === 'stop_with_remainder') {
216
+ expect(r2.remainder).not.toContain(' ');
217
+ }
218
+ });
219
+ });
220
+
221
+ // ── Interrogative gate (covers legacy isInterrogative behavior) ─────────
222
+
223
+ describe('interrogative gate', () => {
224
+ test.each([
225
+ 'how do I stop recording?',
226
+ 'how do I record my screen?',
227
+ 'what does screen recording do?',
228
+ 'why is screen recording not working?',
229
+ 'when should I stop recording?',
230
+ 'where does the recording file go?',
231
+ 'which display should I record?',
232
+ 'What is the screen recording feature?',
233
+ 'How do I start recording on Mac?',
234
+ 'how can I record my screen?',
235
+ 'why did the recording stop?',
236
+ ])('returns none for question: "%s"', (text) => {
237
+ expect(resolveRecordingIntent(text)).toEqual({ kind: 'none' });
238
+ });
239
+
240
+ test.each([
241
+ 'record my screen',
242
+ 'stop recording',
243
+ 'open Chrome and record my screen',
244
+ 'can you record my screen?',
245
+ 'could you stop recording please',
246
+ 'start recording',
247
+ 'please record my screen',
248
+ ])('does not block command: "%s"', (text) => {
249
+ expect(resolveRecordingIntent(text).kind).not.toBe('none');
250
+ });
251
+
252
+ test('strips dynamic name before checking interrogative', () => {
253
+ expect(resolveRecordingIntent('Nova, how do I stop recording?', ['Nova'])).toEqual({ kind: 'none' });
254
+ expect(resolveRecordingIntent('Nova, record my screen', ['Nova']).kind).toBe('start_only');
255
+ });
256
+
257
+ test('handles polite prefix before question word', () => {
258
+ expect(resolveRecordingIntent('please, how do I stop recording?')).toEqual({ kind: 'none' });
259
+ expect(resolveRecordingIntent('hey, what does screen recording do?')).toEqual({ kind: 'none' });
260
+ });
261
+
262
+ // Interrogative gate for new patterns
263
+ test('returns none for question about restart', () => {
264
+ expect(resolveRecordingIntent('how do I restart recording?')).toEqual({ kind: 'none' });
265
+ });
266
+
267
+ test('returns none for question about pause', () => {
268
+ expect(resolveRecordingIntent('how can I pause recording?')).toEqual({ kind: 'none' });
269
+ });
270
+
271
+ test('returns none for question about resume', () => {
272
+ expect(resolveRecordingIntent('how do I resume recording?')).toEqual({ kind: 'none' });
273
+ });
274
+
275
+ // ── Indirect informational patterns ───────────────────────────────────
276
+
277
+ test.each([
278
+ 'can you tell me how to stop recording?',
279
+ 'could you tell me how to stop recording?',
280
+ 'would you tell me how to stop recording?',
281
+ 'explain how to stop the recording',
282
+ 'tell me how recording works',
283
+ 'describe how screen recording works',
284
+ 'show me how to record my screen',
285
+ 'is there a way to stop recording?',
286
+ 'is there a method to pause recording?',
287
+ 'are there any ways to record my screen?',
288
+ "I'd like to know how to stop recording",
289
+ "I would like to know how to pause the recording",
290
+ 'I want to know how to start recording',
291
+ 'do you know how to start recording?',
292
+ 'can I learn how to record my screen?',
293
+ 'can you explain how to record my screen?',
294
+ 'tell me about how screen recording works',
295
+ 'explain to me how to stop recording',
296
+ 'tell me what screen recording does',
297
+ 'describe how to start a recording',
298
+ 'please, tell me how to stop recording',
299
+ 'hey, can you explain how to record my screen?',
300
+ ])('returns none for indirect informational question: "%s"', (text) => {
301
+ expect(resolveRecordingIntent(text)).toEqual({ kind: 'none' });
302
+ });
303
+
304
+ test('indirect informational with dynamic name returns none', () => {
305
+ expect(resolveRecordingIntent('Nova, can you tell me how to stop recording?', ['Nova'])).toEqual({ kind: 'none' });
306
+ expect(resolveRecordingIntent('Nova, explain how to record my screen', ['Nova'])).toEqual({ kind: 'none' });
307
+ expect(resolveRecordingIntent('hey Nova, is there a way to stop recording?', ['Nova'])).toEqual({ kind: 'none' });
308
+ });
309
+
310
+ // ── Polite imperatives that should still execute (NOT none) ──────────
311
+
312
+ test.each([
313
+ ['can you stop recording?', 'stop_only'],
314
+ ['could you record my screen?', 'start_only'],
315
+ ['can you pause the recording?', 'pause_only'],
316
+ ['would you resume recording?', 'resume_only'],
317
+ ['please stop recording', 'stop_only'],
318
+ ['can you start recording?', 'start_only'],
319
+ ['could you stop the recording please', 'stop_only'],
320
+ ] as const)('polite imperative "%s" resolves to %s (not none)', (text, expected) => {
321
+ expect(resolveRecordingIntent(text).kind).toBe(expected);
322
+ });
323
+ });
324
+
325
+ // ── Mixed-intent with remainder (regression coverage) ────────────────────
326
+
327
+ describe('mixed-intent with remainder', () => {
328
+ test('"stop recording and start a new one and open safari" → restart_with_remainder', () => {
329
+ const result = resolveRecordingIntent('stop recording and start a new one and open safari');
330
+ expect(result.kind).toBe('restart_with_remainder');
331
+ if (result.kind === 'restart_with_remainder') {
332
+ expect(result.remainder).toContain('open safari');
333
+ }
334
+ });
335
+
336
+ test('"record my screen and open Chrome and go to google.com" → start_with_remainder', () => {
337
+ const result = resolveRecordingIntent('record my screen and open Chrome and go to google.com');
338
+ expect(result.kind).toBe('start_with_remainder');
339
+ if (result.kind === 'start_with_remainder') {
340
+ expect(result.remainder).toContain('open Chrome');
341
+ expect(result.remainder).toContain('google.com');
342
+ }
343
+ });
344
+
345
+ test('"stop recording and send the file to Bob" → stop_with_remainder', () => {
346
+ const result = resolveRecordingIntent('stop recording and send the file to Bob');
347
+ expect(result.kind).toBe('stop_with_remainder');
348
+ if (result.kind === 'stop_with_remainder') {
349
+ expect(result.remainder).toContain('send the file to Bob');
350
+ }
351
+ });
352
+ });
353
+
354
+ // ── Dynamic names ──────────────────────────────────────────────────────────
355
+
356
+ describe('dynamic name handling', () => {
357
+ test.each([
358
+ ['Nova, record my screen', ['Nova'], 'start_only'],
359
+ ['hey Nova, start recording', ['Nova'], 'start_only'],
360
+ ['hey, Nova, start recording', ['Nova'], 'start_only'],
361
+ ['Nova, stop recording', ['Nova'], 'stop_only'],
362
+ ['Nova, hello', ['Nova'], 'none'],
363
+ ] as const)('"%s" with names %j resolves to %s', (text, names, expected) => {
364
+ expect(resolveRecordingIntent(text, [...names]).kind).toBe(expected);
365
+ });
366
+
367
+ test('mixed intent with dynamic name extracts remainder', () => {
368
+ const result = resolveRecordingIntent('Nova, open Safari and record my screen', ['Nova']);
369
+ expect(result.kind).toBe('start_with_remainder');
370
+ if (result.kind === 'start_with_remainder') {
371
+ expect(result.remainder).toContain('open Safari');
372
+ }
373
+ });
374
+
375
+ test('dynamic name stripping is case-insensitive', () => {
376
+ expect(resolveRecordingIntent('nova, record my screen', ['Nova']).kind).toBe('start_only');
377
+ expect(resolveRecordingIntent('NOVA, stop recording', ['Nova']).kind).toBe('stop_only');
378
+ expect(resolveRecordingIntent('Hey NOVA, start recording', ['nova']).kind).toBe('start_only');
379
+ });
380
+
381
+ test('handles multiple dynamic names', () => {
382
+ expect(resolveRecordingIntent('Jarvis, record my screen', ['Nova', 'Jarvis']).kind).toBe('start_only');
383
+ expect(resolveRecordingIntent('Nova, stop recording', ['Nova', 'Jarvis']).kind).toBe('stop_only');
384
+ });
385
+
386
+ test('handles empty dynamic names array', () => {
387
+ expect(resolveRecordingIntent('record my screen', []).kind).toBe('start_only');
388
+ expect(resolveRecordingIntent('stop recording', []).kind).toBe('stop_only');
389
+ });
390
+
391
+ test('handles colon separator after name', () => {
392
+ expect(resolveRecordingIntent('Nova: record my screen', ['Nova']).kind).toBe('start_only');
393
+ });
394
+
395
+ test('interrogative with name prefix returns none', () => {
396
+ expect(resolveRecordingIntent('hey Nova, how do I stop recording?', ['Nova'])).toEqual({ kind: 'none' });
397
+ });
398
+ });
399
+
400
+ // ── Start + stop combined ──────────────────────────────────────────────────
401
+
402
+ describe('combined start and stop', () => {
403
+ test('start and stop: "stop recording and record my screen"', () => {
404
+ const result = resolveRecordingIntent('stop recording and record my screen');
405
+ expect(result.kind).toBe('start_and_stop_only');
406
+ });
407
+
408
+ test('start and stop: "stop recording and start recording"', () => {
409
+ const result = resolveRecordingIntent('stop recording and start recording');
410
+ expect(result.kind).toBe('start_and_stop_only');
411
+ });
412
+ });
413
+
414
+ // ── Restart compound detection ────────────────────────────────────────────
415
+
416
+ describe('restart compound detection', () => {
417
+ test('"restart the recording" → restart_only', () => {
418
+ expect(resolveRecordingIntent('restart the recording')).toEqual({ kind: 'restart_only' });
419
+ });
420
+
421
+ test('"restart recording" → restart_only', () => {
422
+ expect(resolveRecordingIntent('restart recording')).toEqual({ kind: 'restart_only' });
423
+ });
424
+
425
+ test('"redo the recording" → restart_only', () => {
426
+ expect(resolveRecordingIntent('redo the recording')).toEqual({ kind: 'restart_only' });
427
+ });
428
+
429
+ test('"stop recording and start a new one" → restart_only', () => {
430
+ expect(resolveRecordingIntent('stop recording and start a new one')).toEqual({ kind: 'restart_only' });
431
+ });
432
+
433
+ test('"stop the recording and start a new one" → restart_only', () => {
434
+ expect(resolveRecordingIntent('stop the recording and start a new one')).toEqual({ kind: 'restart_only' });
435
+ });
436
+
437
+ test('"stop the recording and begin a fresh" → restart_only', () => {
438
+ expect(resolveRecordingIntent('stop the recording and begin a fresh')).toEqual({ kind: 'restart_only' });
439
+ });
440
+
441
+ test('"stop and restart the recording" → restart_only', () => {
442
+ expect(resolveRecordingIntent('stop and restart the recording')).toEqual({ kind: 'restart_only' });
443
+ });
444
+
445
+ test('"stop recording and start a new" → restart_only', () => {
446
+ expect(resolveRecordingIntent('stop recording and start a new')).toEqual({ kind: 'restart_only' });
447
+ });
448
+
449
+ test('"stop recording and start another" → restart_only', () => {
450
+ expect(resolveRecordingIntent('stop recording and start another')).toEqual({ kind: 'restart_only' });
451
+ });
452
+
453
+ test('"stop recording and start another." → restart_only (trailing period)', () => {
454
+ expect(resolveRecordingIntent('stop recording and start another.')).toEqual({ kind: 'restart_only' });
455
+ });
456
+
457
+ test('"stop recording and start a new!" → restart_only (trailing exclamation)', () => {
458
+ expect(resolveRecordingIntent('stop recording and start a new!')).toEqual({ kind: 'restart_only' });
459
+ });
460
+
461
+ test('restart with remainder: "restart recording and open safari"', () => {
462
+ const result = resolveRecordingIntent('restart recording and open safari');
463
+ expect(result.kind).toBe('restart_with_remainder');
464
+ if (result.kind === 'restart_with_remainder') {
465
+ expect(result.remainder).toContain('open safari');
466
+ }
467
+ });
33
468
 
34
- test.each([
35
- '',
36
- 'hello world',
37
- 'open Safari',
38
- 'stop recording',
39
- 'take a screenshot',
40
- 'what time is it?',
41
- 'record a note',
42
- 'make a note',
43
- 'start the timer',
44
- ])('does not detect recording intent in "%s"', (text) => {
45
- expect(detectRecordingIntent(text)).toBe(false);
46
- });
469
+ test('restart with polite fillers resolves as restart_only', () => {
470
+ expect(resolveRecordingIntent('please restart the recording')).toEqual({ kind: 'restart_only' });
471
+ expect(resolveRecordingIntent('can you restart recording')).toEqual({ kind: 'restart_only' });
472
+ });
47
473
 
48
- test('is case-insensitive', () => {
49
- expect(detectRecordingIntent('RECORD MY SCREEN')).toBe(true);
50
- expect(detectRecordingIntent('Screen Recording')).toBe(true);
51
- expect(detectRecordingIntent('START RECORDING')).toBe(true);
52
- });
53
- });
474
+ test('restart takes precedence over independent start/stop', () => {
475
+ // "stop recording and start a new one" should be restart, not start_and_stop
476
+ const result = resolveRecordingIntent('stop recording and start a new one');
477
+ expect(result.kind).toBe('restart_only');
478
+ });
54
479
 
55
- // ─── isRecordingOnly ────────────────────────────────────────────────────────
56
-
57
- describe('isRecordingOnly', () => {
58
- test.each([
59
- 'record my screen',
60
- 'Record my screen',
61
- 'start recording',
62
- 'screen recording',
63
- 'begin recording',
64
- 'capture my screen',
65
- 'make a recording',
66
- ])('returns true for pure recording request "%s"', (text) => {
67
- expect(isRecordingOnly(text)).toBe(true);
68
- });
480
+ test('"stop recording and start a new recording" → restart_only', () => {
481
+ expect(resolveRecordingIntent('stop recording and start a new recording')).toEqual({ kind: 'restart_only' });
482
+ });
69
483
 
70
- test('returns true when polite fillers surround the recording request', () => {
71
- expect(isRecordingOnly('please record my screen')).toBe(true);
72
- expect(isRecordingOnly('can you start recording')).toBe(true);
73
- expect(isRecordingOnly('could you record my screen please')).toBe(true);
74
- expect(isRecordingOnly('hey, start recording now')).toBe(true);
75
- expect(isRecordingOnly('just record my screen, thanks')).toBe(true);
76
- expect(isRecordingOnly('can you start recording?')).toBe(true);
77
- });
78
-
79
- test.each([
80
- 'record my screen and then open Safari',
81
- 'do this task and record my screen',
82
- 'record my screen while I work on the document',
83
- 'open Chrome and start recording',
84
- 'record my screen and send it to Bob',
85
- ])('returns false for mixed-intent "%s"', (text) => {
86
- expect(isRecordingOnly(text)).toBe(false);
87
- });
484
+ test('"stop the recording and start another recording" → restart_only', () => {
485
+ expect(resolveRecordingIntent('stop the recording and start another recording')).toEqual({ kind: 'restart_only' });
486
+ });
88
487
 
89
- test('returns false for empty or unrelated text', () => {
90
- expect(isRecordingOnly('')).toBe(false);
91
- expect(isRecordingOnly('hello world')).toBe(false);
92
- expect(isRecordingOnly('open Safari')).toBe(false);
93
- });
488
+ // False positive guards: "start another/new <non-recording>" should NOT trigger restart
489
+ test('"stop recording and start another tab" should NOT trigger restart', () => {
490
+ const result = resolveRecordingIntent('stop recording and start another tab');
491
+ expect(result.kind).toBe('stop_with_remainder');
492
+ });
94
493
 
95
- test('handles punctuation in recording-only prompts', () => {
96
- expect(isRecordingOnly('record my screen!')).toBe(true);
97
- expect(isRecordingOnly('start recording.')).toBe(true);
98
- expect(isRecordingOnly('screen recording?')).toBe(true);
99
- });
100
- });
101
-
102
- // ─── detectStopRecordingIntent ──────────────────────────────────────────────
103
-
104
- describe('detectStopRecordingIntent', () => {
105
- test.each([
106
- 'stop recording',
107
- 'stop the recording',
108
- 'end recording',
109
- 'end the recording',
110
- 'finish recording',
111
- 'finish the recording',
112
- 'halt recording',
113
- 'halt the recording',
114
- ])('detects stop intent in "%s"', (text) => {
115
- expect(detectStopRecordingIntent(text)).toBe(true);
116
- });
117
-
118
- test.each([
119
- '',
120
- 'hello world',
121
- 'stop it',
122
- 'end it',
123
- 'quit',
124
- 'record my screen',
125
- 'start recording',
126
- 'take a screenshot',
127
- 'stop the music',
128
- ])('does not detect stop intent in "%s"', (text) => {
129
- expect(detectStopRecordingIntent(text)).toBe(false);
130
- });
131
-
132
- test('is case-insensitive', () => {
133
- expect(detectStopRecordingIntent('STOP RECORDING')).toBe(true);
134
- expect(detectStopRecordingIntent('Stop The Recording')).toBe(true);
135
- expect(detectStopRecordingIntent('END RECORDING')).toBe(true);
136
- });
137
- });
138
-
139
- // ─── stripRecordingIntent ───────────────────────────────────────────────────
140
-
141
- describe('stripRecordingIntent', () => {
142
- test('removes recording clause from mixed-intent prompt', () => {
143
- expect(stripRecordingIntent('open Safari and record my screen')).toBe('open Safari');
144
- expect(stripRecordingIntent('open Safari and also record my screen')).toBe('open Safari');
145
- });
146
-
147
- test('removes start recording clause', () => {
148
- expect(stripRecordingIntent('do this task and start recording')).toBe('do this task');
149
- });
150
-
151
- test('removes begin recording clause', () => {
152
- expect(stripRecordingIntent('write a report and begin recording')).toBe('write a report');
153
- });
154
-
155
- test('removes capture clause', () => {
156
- expect(stripRecordingIntent('check email and capture my screen')).toBe('check email');
157
- });
158
-
159
- test('removes "while" phrased recording clauses', () => {
160
- const result = stripRecordingIntent('do this task while recording the screen');
161
- // The while-recording pattern should be removed
162
- expect(result).not.toContain('recording');
163
- });
164
-
165
- test('returns empty string for recording-only text after stripping', () => {
166
- const result = stripRecordingIntent('record my screen');
167
- // After stripping the recording clause, only the unmatched part remains
168
- expect(result.trim().length).toBeLessThanOrEqual(result.length);
169
- });
170
-
171
- test('cleans up double spaces', () => {
172
- const result = stripRecordingIntent('open Safari and also record my screen please');
173
- expect(result).not.toContain(' ');
174
- });
494
+ test('"stop recording and start another window" should NOT trigger restart', () => {
495
+ const result = resolveRecordingIntent('stop recording and start another window');
496
+ expect(result.kind).toBe('stop_with_remainder');
497
+ });
175
498
 
176
- test('returns unrelated text unchanged', () => {
177
- expect(stripRecordingIntent('hello world')).toBe('hello world');
178
- expect(stripRecordingIntent('open Safari')).toBe('open Safari');
179
- });
499
+ test('"stop recording and start a new project" should NOT trigger restart', () => {
500
+ const result = resolveRecordingIntent('stop recording and start a new project');
501
+ expect(result.kind).toBe('stop_with_remainder');
502
+ });
180
503
 
181
- test('handles empty string', () => {
182
- expect(stripRecordingIntent('')).toBe('');
504
+ test('"stop the recording and begin a fresh session" should NOT trigger restart', () => {
505
+ const result = resolveRecordingIntent('stop the recording and begin a fresh session');
506
+ expect(result.kind).toBe('stop_with_remainder');
507
+ });
183
508
  });
184
- });
185
509
 
186
- // ─── stripStopRecordingIntent ───────────────────────────────────────────────
510
+ // ── Pause detection ───────────────────────────────────────────────────────
187
511
 
188
- describe('stripStopRecordingIntent', () => {
189
- test('removes stop recording clause from mixed-intent prompt', () => {
190
- expect(stripStopRecordingIntent('open Chrome and stop recording')).toBe('open Chrome');
191
- expect(stripStopRecordingIntent('open Chrome and also stop recording')).toBe('open Chrome');
192
- });
512
+ describe('pause detection', () => {
513
+ test('"pause recording" pause_only', () => {
514
+ expect(resolveRecordingIntent('pause recording')).toEqual({ kind: 'pause_only' });
515
+ });
193
516
 
194
- test('removes end recording clause', () => {
195
- expect(stripStopRecordingIntent('save the file and end the recording')).toBe('save the file');
196
- });
517
+ test('"pause the recording" → pause_only', () => {
518
+ expect(resolveRecordingIntent('pause the recording')).toEqual({ kind: 'pause_only' });
519
+ });
197
520
 
198
- test('removes finish recording clause', () => {
199
- expect(stripStopRecordingIntent('close the browser and finish recording')).toBe('close the browser');
521
+ test('pause with polite fillers resolves as pause_only', () => {
522
+ expect(resolveRecordingIntent('please pause the recording')).toEqual({ kind: 'pause_only' });
523
+ expect(resolveRecordingIntent('can you pause recording')).toEqual({ kind: 'pause_only' });
524
+ });
200
525
  });
201
526
 
202
- test('removes halt recording clause', () => {
203
- expect(stripStopRecordingIntent('do this and halt the recording')).toBe('do this');
204
- });
527
+ // ── Resume detection ──────────────────────────────────────────────────────
205
528
 
206
- test('returns unrelated text unchanged', () => {
207
- expect(stripStopRecordingIntent('hello world')).toBe('hello world');
208
- expect(stripStopRecordingIntent('open Safari')).toBe('open Safari');
209
- });
529
+ describe('resume detection', () => {
530
+ test('"resume recording" → resume_only', () => {
531
+ expect(resolveRecordingIntent('resume recording')).toEqual({ kind: 'resume_only' });
532
+ });
210
533
 
211
- test('handles empty string', () => {
212
- expect(stripStopRecordingIntent('')).toBe('');
213
- });
534
+ test('"resume the recording" → resume_only', () => {
535
+ expect(resolveRecordingIntent('resume the recording')).toEqual({ kind: 'resume_only' });
536
+ });
214
537
 
215
- test('cleans up double spaces', () => {
216
- const result = stripStopRecordingIntent('open Safari and also stop recording please');
217
- expect(result).not.toContain(' ');
218
- });
219
- });
538
+ test('"unpause the recording" → resume_only', () => {
539
+ expect(resolveRecordingIntent('unpause the recording')).toEqual({ kind: 'resume_only' });
540
+ });
220
541
 
221
- // ─── isStopRecordingOnly ────────────────────────────────────────────────────
222
-
223
- describe('isStopRecordingOnly', () => {
224
- test.each([
225
- 'stop recording',
226
- 'stop the recording',
227
- 'end recording',
228
- 'end the recording',
229
- 'finish recording',
230
- 'halt recording',
231
- ])('returns true for pure stop-recording request "%s"', (text) => {
232
- expect(isStopRecordingOnly(text)).toBe(true);
542
+ test('resume with polite fillers resolves as resume_only', () => {
543
+ expect(resolveRecordingIntent('please resume the recording')).toEqual({ kind: 'resume_only' });
544
+ });
233
545
  });
234
546
 
235
- test('returns true when polite fillers surround the stop request', () => {
236
- expect(isStopRecordingOnly('please stop recording')).toBe(true);
237
- expect(isStopRecordingOnly('can you stop the recording?')).toBe(true);
238
- expect(isStopRecordingOnly('could you end the recording please')).toBe(true);
239
- expect(isStopRecordingOnly('stop the recording now')).toBe(true);
240
- expect(isStopRecordingOnly('just stop recording, thanks')).toBe(true);
241
- });
547
+ // ── False positive guards ─────────────────────────────────────────────────
242
548
 
243
- test.each([
244
- 'stop recording and open Chrome',
245
- 'end the recording and then close Safari',
246
- 'how do I stop recording?',
247
- ])('returns false for mixed-intent or questioning "%s"', (text) => {
248
- expect(isStopRecordingOnly(text)).toBe(false);
249
- });
549
+ describe('false positive guards', () => {
550
+ test('"I recorded a restart" → none', () => {
551
+ expect(resolveRecordingIntent('I recorded a restart')).toEqual({ kind: 'none' });
552
+ });
250
553
 
251
- test('returns false for ambiguous phrases', () => {
252
- expect(isStopRecordingOnly('end it')).toBe(false);
253
- expect(isStopRecordingOnly('stop')).toBe(false);
254
- expect(isStopRecordingOnly('quit')).toBe(false);
255
- });
554
+ test('"the pause button is broken" → none (no recording mention)', () => {
555
+ expect(resolveRecordingIntent('the pause button is broken')).toEqual({ kind: 'none' });
556
+ });
256
557
 
257
- test('returns false for empty or unrelated text', () => {
258
- expect(isStopRecordingOnly('')).toBe(false);
259
- expect(isStopRecordingOnly('hello world')).toBe(false);
260
- expect(isStopRecordingOnly('open Safari')).toBe(false);
558
+ test('"resume my work" none (no recording mention)', () => {
559
+ expect(resolveRecordingIntent('resume my work')).toEqual({ kind: 'none' });
560
+ });
261
561
  });
262
562
 
263
- test('handles punctuation', () => {
264
- expect(isStopRecordingOnly('stop recording!')).toBe(true);
265
- expect(isStopRecordingOnly('stop recording.')).toBe(true);
266
- expect(isStopRecordingOnly('end the recording?')).toBe(true);
267
- });
268
- });
563
+ // ── No recording intent ────────────────────────────────────────────────────
269
564
 
270
- // ─── classifyRecordingIntent ────────────────────────────────────────────────
271
-
272
- describe('classifyRecordingIntent', () => {
273
- // Basic classification
274
- test.each([
275
- ['record my screen', 'start_only'],
276
- ['stop recording', 'stop_only'],
277
- ['open Safari and record my screen', 'mixed'],
278
- ['hello world', 'none'],
279
- ['', 'none'],
280
- ] as const)('basic: "%s" → %s', (text, expected) => {
281
- expect(classifyRecordingIntent(text)).toBe(expected);
565
+ describe('no recording intent', () => {
566
+ test.each([
567
+ 'open Safari',
568
+ 'I broke the record',
569
+ '',
570
+ 'hello world',
571
+ ])('returns none for "%s"', (text) => {
572
+ expect(resolveRecordingIntent(text)).toEqual({ kind: 'none' });
573
+ });
282
574
  });
283
575
 
284
- // Dynamic name stripping
285
- test.each([
286
- ['Nova, record my screen', ['Nova'], 'start_only'],
287
- ['hey Nova, start recording', ['Nova'], 'start_only'],
288
- ['hey, Nova, start recording', ['Nova'], 'start_only'],
289
- ['Nova, stop recording', ['Nova'], 'stop_only'],
290
- ['Nova, open Safari and record my screen', ['Nova'], 'mixed'],
291
- ['Nova, hello', ['Nova'], 'none'],
292
- ] as const)('dynamic names: "%s" with %j → %s', (text, names, expected) => {
293
- expect(classifyRecordingIntent(text, [...names])).toBe(expected);
294
- });
576
+ // ── Works without dynamic names parameter ──────────────────────────────────
295
577
 
296
- // No dynamic names (backwards compat)
297
578
  test('works without dynamic names parameter', () => {
298
- expect(classifyRecordingIntent('record my screen')).toBe('start_only');
299
- expect(classifyRecordingIntent('stop recording')).toBe('stop_only');
300
- });
301
-
302
- // With fillers
303
- test('handles filler words correctly', () => {
304
- expect(classifyRecordingIntent('please record my screen')).toBe('start_only');
305
- expect(classifyRecordingIntent('can you stop recording?')).toBe('stop_only');
306
- });
307
-
308
- // Both start and stop → mixed
309
- test('classifies as mixed when both start and stop patterns are present', () => {
310
- expect(classifyRecordingIntent('start recording and then stop recording')).toBe('mixed');
311
- expect(classifyRecordingIntent('record my screen and stop recording')).toBe('mixed');
312
- });
313
-
314
- // Edge cases
315
- test('classifies as mixed when stop-recording has additional task', () => {
316
- expect(classifyRecordingIntent('stop recording and open Chrome')).toBe('mixed');
317
- });
318
-
319
- // Case insensitivity with dynamic names
320
- test('dynamic name stripping is case-insensitive', () => {
321
- expect(classifyRecordingIntent('nova, record my screen', ['Nova'])).toBe('start_only');
322
- expect(classifyRecordingIntent('NOVA, stop recording', ['Nova'])).toBe('stop_only');
323
- expect(classifyRecordingIntent('Hey NOVA, start recording', ['nova'])).toBe('start_only');
579
+ expect(resolveRecordingIntent('record my screen')).toEqual({ kind: 'start_only' });
580
+ expect(resolveRecordingIntent('stop recording')).toEqual({ kind: 'stop_only' });
324
581
  });
582
+ });
325
583
 
326
- // Multiple dynamic names
327
- test('handles multiple dynamic names', () => {
328
- expect(classifyRecordingIntent('Jarvis, record my screen', ['Nova', 'Jarvis'])).toBe(
329
- 'start_only',
584
+ // ─── executeRecordingIntent ─────────────────────────────────────────────────
585
+
586
+ describe('executeRecordingIntent', () => {
587
+ // Mock the recording handlers module
588
+ const mockHandleRecordingStart = mock((): string | null => 'mock-recording-id');
589
+ const mockHandleRecordingStop = mock((): string | undefined => 'mock-recording-id');
590
+ const mockHandleRecordingRestart = mock((): { initiated: boolean; operationToken?: string; responseText: string } => ({
591
+ initiated: true,
592
+ operationToken: 'mock-token',
593
+ responseText: 'Restarting screen recording.',
594
+ }));
595
+ const mockHandleRecordingPause = mock((): string | undefined => 'mock-recording-id');
596
+ const mockHandleRecordingResume = mock((): string | undefined => 'mock-recording-id');
597
+
598
+ mock.module('../daemon/handlers/recording.js', () => ({
599
+ handleRecordingStart: mockHandleRecordingStart,
600
+ handleRecordingStop: mockHandleRecordingStop,
601
+ handleRecordingRestart: mockHandleRecordingRestart,
602
+ handleRecordingPause: mockHandleRecordingPause,
603
+ handleRecordingResume: mockHandleRecordingResume,
604
+ isRecordingIdle: () => true,
605
+ }));
606
+
607
+ // Dynamically import so the mock takes effect
608
+ let executeRecordingIntent: typeof import('../daemon/recording-executor.js').executeRecordingIntent;
609
+
610
+ // Must await the dynamic import before running tests
611
+ const setupPromise = import('../daemon/recording-executor.js').then((mod) => {
612
+ executeRecordingIntent = mod.executeRecordingIntent;
613
+ });
614
+
615
+ const mockContext = {
616
+ conversationId: 'conv-123',
617
+ socket: {} as any,
618
+ ctx: {} as any,
619
+ };
620
+
621
+ beforeEach(async () => {
622
+ await setupPromise;
623
+ mockHandleRecordingStart.mockReset();
624
+ mockHandleRecordingStop.mockReset();
625
+ mockHandleRecordingRestart.mockReset();
626
+ mockHandleRecordingPause.mockReset();
627
+ mockHandleRecordingResume.mockReset();
628
+ // Default: start succeeds (returns recording ID)
629
+ mockHandleRecordingStart.mockReturnValue('mock-recording-id');
630
+ // Default: stop succeeds (returns recording ID)
631
+ mockHandleRecordingStop.mockReturnValue('mock-recording-id');
632
+ // Default: restart succeeds
633
+ mockHandleRecordingRestart.mockReturnValue({
634
+ initiated: true,
635
+ operationToken: 'mock-token',
636
+ responseText: 'Restarting screen recording.',
637
+ });
638
+ // Default: pause succeeds
639
+ mockHandleRecordingPause.mockReturnValue('mock-recording-id');
640
+ // Default: resume succeeds
641
+ mockHandleRecordingResume.mockReturnValue('mock-recording-id');
642
+ });
643
+
644
+ test('none → returns { handled: false }', () => {
645
+ const result = executeRecordingIntent({ kind: 'none' }, mockContext);
646
+ expect(result).toEqual({ handled: false });
647
+ });
648
+
649
+ test('start_only → calls handleRecordingStart, returns handled with start text', () => {
650
+ const result = executeRecordingIntent({ kind: 'start_only' }, mockContext);
651
+ expect(mockHandleRecordingStart).toHaveBeenCalledTimes(1);
652
+ expect(result).toEqual({
653
+ handled: true,
654
+ recordingStarted: true,
655
+ responseText: 'Starting screen recording.',
656
+ });
657
+ });
658
+
659
+ test('start_only when recording already active → returns handled with already-active text', () => {
660
+ mockHandleRecordingStart.mockReturnValue(null);
661
+ const result = executeRecordingIntent({ kind: 'start_only' }, mockContext);
662
+ expect(mockHandleRecordingStart).toHaveBeenCalledTimes(1);
663
+ expect(result).toEqual({
664
+ handled: true,
665
+ recordingStarted: false,
666
+ responseText: 'A recording is already active.',
667
+ });
668
+ });
669
+
670
+ test('stop_only → calls handleRecordingStop, returns handled with stop text', () => {
671
+ const result = executeRecordingIntent({ kind: 'stop_only' }, mockContext);
672
+ expect(mockHandleRecordingStop).toHaveBeenCalledTimes(1);
673
+ expect(result).toEqual({
674
+ handled: true,
675
+ responseText: 'Stopping the recording.',
676
+ });
677
+ });
678
+
679
+ test('stop_only when no active recording → returns handled with no-active text', () => {
680
+ mockHandleRecordingStop.mockReturnValue(undefined);
681
+ const result = executeRecordingIntent({ kind: 'stop_only' }, mockContext);
682
+ expect(result).toEqual({
683
+ handled: true,
684
+ responseText: 'No active recording to stop.',
685
+ });
686
+ });
687
+
688
+ test('start_with_remainder → returns not handled with remainder and pendingStart', () => {
689
+ const result = executeRecordingIntent(
690
+ { kind: 'start_with_remainder', remainder: 'open Safari' },
691
+ mockContext,
330
692
  );
331
- expect(classifyRecordingIntent('Nova, stop recording', ['Nova', 'Jarvis'])).toBe('stop_only');
332
- });
333
-
334
- // Empty dynamic names array
335
- test('handles empty dynamic names array', () => {
336
- expect(classifyRecordingIntent('record my screen', [])).toBe('start_only');
337
- expect(classifyRecordingIntent('stop recording', [])).toBe('stop_only');
338
- });
339
-
340
- // Name with colon separator
341
- test('handles colon separator after name', () => {
342
- expect(classifyRecordingIntent('Nova: record my screen', ['Nova'])).toBe('start_only');
693
+ expect(result).toEqual({
694
+ handled: false,
695
+ remainderText: 'open Safari',
696
+ pendingStart: true,
697
+ });
343
698
  });
344
- });
345
699
 
346
- // ─── isInterrogative ──────────────────────────────────────────────────────────
347
-
348
- describe('isInterrogative', () => {
349
- // Questions about recording — should return true
350
- test.each([
351
- 'how do I stop recording?',
352
- 'how do I record my screen?',
353
- 'what does screen recording do?',
354
- 'why is screen recording not working?',
355
- 'when should I stop recording?',
356
- 'where does the recording file go?',
357
- 'which display should I record?',
358
- 'What is the screen recording feature?',
359
- 'How do I start recording on Mac?',
360
- ])('returns true for question: "%s"', (text) => {
361
- expect(isInterrogative(text)).toBe(true);
700
+ test('stop_with_remainder returns not handled with remainder and pendingStop', () => {
701
+ const result = executeRecordingIntent(
702
+ { kind: 'stop_with_remainder', remainder: 'open Chrome' },
703
+ mockContext,
704
+ );
705
+ expect(result).toEqual({
706
+ handled: false,
707
+ remainderText: 'open Chrome',
708
+ pendingStop: true,
709
+ });
710
+ });
711
+
712
+ test('start_and_stop_only routes through handleRecordingRestart, returns handled', () => {
713
+ const result = executeRecordingIntent({ kind: 'start_and_stop_only' }, mockContext);
714
+ expect(mockHandleRecordingRestart).toHaveBeenCalledTimes(1);
715
+ expect(result).toEqual({
716
+ handled: true,
717
+ recordingStarted: true,
718
+ responseText: 'Stopping current recording and starting a new one.',
719
+ });
720
+ });
721
+
722
+ test('start_and_stop_only when restart fails → returns handled with restart failure text', () => {
723
+ mockHandleRecordingRestart.mockReturnValue({
724
+ initiated: false,
725
+ responseText: 'No active recording to restart.',
726
+ });
727
+ const result = executeRecordingIntent({ kind: 'start_and_stop_only' }, mockContext);
728
+ expect(mockHandleRecordingRestart).toHaveBeenCalledTimes(1);
729
+ expect(result).toEqual({
730
+ handled: true,
731
+ recordingStarted: false,
732
+ responseText: 'No active recording to restart.',
733
+ });
734
+ });
735
+
736
+ test('start_and_stop_with_remainder → returns not handled with remainder and pendingStart when idle', () => {
737
+ const result = executeRecordingIntent(
738
+ { kind: 'start_and_stop_with_remainder', remainder: 'open Safari' },
739
+ mockContext,
740
+ );
741
+ expect(result).toEqual({
742
+ handled: false,
743
+ remainderText: 'open Safari',
744
+ pendingStart: true,
745
+ });
362
746
  });
363
747
 
364
- // Imperative commands should return false
365
- test.each([
366
- 'record my screen',
367
- 'stop recording',
368
- 'open Chrome and record my screen',
369
- 'stop recording and close the browser',
370
- 'can you record my screen?',
371
- 'could you stop recording please',
372
- 'start recording',
373
- 'please record my screen',
374
- ])('returns false for command: "%s"', (text) => {
375
- expect(isInterrogative(text)).toBe(false);
376
- });
748
+ // ── New intent kinds ──────────────────────────────────────────────────────
377
749
 
378
- // With dynamic names strips name prefix first
379
- test('strips dynamic name before checking', () => {
380
- expect(isInterrogative('Nova, how do I stop recording?', ['Nova'])).toBe(true);
381
- expect(isInterrogative('Nova, record my screen', ['Nova'])).toBe(false);
750
+ test('restart_only returns handled with restart text', () => {
751
+ const result = executeRecordingIntent({ kind: 'restart_only' }, mockContext);
752
+ expect(result).toEqual({
753
+ handled: true,
754
+ responseText: 'Restarting screen recording.',
755
+ });
382
756
  });
383
757
 
384
- // Polite prefix + question
385
- test('handles polite prefix before question word', () => {
386
- expect(isInterrogative('please, how do I stop recording?')).toBe(true);
387
- expect(isInterrogative('hey, what does screen recording do?')).toBe(true);
758
+ test('restart_with_remainder returns not handled with remainder and pendingRestart', () => {
759
+ const result = executeRecordingIntent(
760
+ { kind: 'restart_with_remainder', remainder: 'and open safari' },
761
+ mockContext,
762
+ );
763
+ expect(result).toEqual({
764
+ handled: false,
765
+ remainderText: 'and open safari',
766
+ pendingRestart: true,
767
+ });
768
+ });
769
+
770
+ test('pause_only → returns handled with pause text', () => {
771
+ const result = executeRecordingIntent({ kind: 'pause_only' }, mockContext);
772
+ expect(result).toEqual({
773
+ handled: true,
774
+ responseText: 'Pausing the recording.',
775
+ });
776
+ });
777
+
778
+ test('resume_only → returns handled with resume text', () => {
779
+ const result = executeRecordingIntent({ kind: 'resume_only' }, mockContext);
780
+ expect(result).toEqual({
781
+ handled: true,
782
+ responseText: 'Resuming the recording.',
783
+ });
388
784
  });
389
785
  });