@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.
- package/ARCHITECTURE.md +17 -3
- package/Dockerfile +1 -1
- package/README.md +2 -0
- package/docs/architecture/scheduling.md +81 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +22 -0
- package/src/__tests__/channel-policy.test.ts +19 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +582 -0
- package/src/__tests__/guardian-outbound-http.test.ts +8 -8
- package/src/__tests__/intent-routing.test.ts +22 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/notification-routing-intent.test.ts +185 -0
- package/src/__tests__/recording-handler.test.ts +191 -31
- package/src/__tests__/recording-intent-fallback.test.ts +180 -0
- package/src/__tests__/recording-intent-handler.test.ts +597 -74
- package/src/__tests__/recording-intent.test.ts +738 -342
- package/src/__tests__/recording-state-machine.test.ts +1109 -0
- package/src/__tests__/reminder-store.test.ts +20 -18
- package/src/__tests__/reminder.test.ts +2 -1
- package/src/channels/config.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -11
- package/src/config/bundled-skills/screen-recording/SKILL.md +91 -12
- package/src/config/system-prompt.ts +5 -0
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/daemon/handlers/config-channels.ts +6 -6
- package/src/daemon/handlers/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +258 -102
- package/src/daemon/handlers/recording.ts +417 -5
- package/src/daemon/handlers/sessions.ts +142 -68
- package/src/daemon/ipc-contract/computer-use.ts +23 -3
- package/src/daemon/ipc-contract/messages.ts +3 -1
- package/src/daemon/ipc-contract/shared.ts +6 -0
- package/src/daemon/ipc-contract-inventory.json +2 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/recording-executor.ts +180 -0
- package/src/daemon/recording-intent-fallback.ts +132 -0
- package/src/daemon/recording-intent.ts +306 -15
- package/src/daemon/session-tool-setup.ts +4 -0
- package/src/memory/conversation-attention-store.ts +5 -5
- package/src/notifications/README.md +69 -1
- package/src/notifications/adapters/sms.ts +80 -0
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +70 -1
- package/src/notifications/decisions-store.ts +24 -0
- package/src/notifications/destination-resolver.ts +2 -1
- package/src/notifications/emit-signal.ts +35 -3
- package/src/notifications/signal.ts +6 -0
- package/src/notifications/types.ts +3 -0
- package/src/runtime/guardian-outbound-actions.ts +9 -9
- package/src/runtime/http-server.ts +7 -7
- package/src/runtime/routes/conversation-attention-routes.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +5 -5
- package/src/schedule/scheduler.ts +15 -3
- package/src/tools/executor.ts +29 -0
- package/src/tools/guardian-control-plane-policy.ts +141 -0
- 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
|
-
|
|
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
|
-
// ───
|
|
15
|
-
|
|
16
|
-
describe('
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
510
|
+
// ── Pause detection ───────────────────────────────────────────────────────
|
|
187
511
|
|
|
188
|
-
describe('
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
});
|
|
512
|
+
describe('pause detection', () => {
|
|
513
|
+
test('"pause recording" → pause_only', () => {
|
|
514
|
+
expect(resolveRecordingIntent('pause recording')).toEqual({ kind: 'pause_only' });
|
|
515
|
+
});
|
|
193
516
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
517
|
+
test('"pause the recording" → pause_only', () => {
|
|
518
|
+
expect(resolveRecordingIntent('pause the recording')).toEqual({ kind: 'pause_only' });
|
|
519
|
+
});
|
|
197
520
|
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
expect(stripStopRecordingIntent('do this and halt the recording')).toBe('do this');
|
|
204
|
-
});
|
|
527
|
+
// ── Resume detection ──────────────────────────────────────────────────────
|
|
205
528
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
529
|
+
describe('resume detection', () => {
|
|
530
|
+
test('"resume recording" → resume_only', () => {
|
|
531
|
+
expect(resolveRecordingIntent('resume recording')).toEqual({ kind: 'resume_only' });
|
|
532
|
+
});
|
|
210
533
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
534
|
+
test('"resume the recording" → resume_only', () => {
|
|
535
|
+
expect(resolveRecordingIntent('resume the recording')).toEqual({ kind: 'resume_only' });
|
|
536
|
+
});
|
|
214
537
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
});
|
|
219
|
-
});
|
|
538
|
+
test('"unpause the recording" → resume_only', () => {
|
|
539
|
+
expect(resolveRecordingIntent('unpause the recording')).toEqual({ kind: 'resume_only' });
|
|
540
|
+
});
|
|
220
541
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
'
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
//
|
|
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(
|
|
299
|
-
expect(
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
expect(
|
|
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
|
-
//
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
expect(
|
|
381
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
});
|