@vellumai/assistant 0.3.13 → 0.3.14
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/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 +584 -0
- 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 +186 -0
- package/src/__tests__/recording-handler.test.ts +191 -31
- package/src/__tests__/recording-intent-fallback.test.ts +181 -0
- package/src/__tests__/recording-intent-handler.test.ts +593 -73
- package/src/__tests__/recording-intent.test.ts +739 -343
- 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/misc.ts +258 -102
- package/src/daemon/handlers/recording.ts +417 -5
- package/src/daemon/handlers/sessions.ts +136 -62
- 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/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/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,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import * as net from 'node:net';
|
|
3
3
|
|
|
4
|
-
import { beforeEach, describe, expect, mock,test } from 'bun:test';
|
|
4
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
5
5
|
|
|
6
6
|
// ─── Mocks (must be before any imports that depend on them) ─────────────────
|
|
7
7
|
|
|
@@ -13,6 +13,8 @@ const noopLogger = {
|
|
|
13
13
|
|
|
14
14
|
mock.module('../util/logger.js', () => ({
|
|
15
15
|
getLogger: () => noopLogger,
|
|
16
|
+
isDebug: () => false,
|
|
17
|
+
truncateForLog: (v: string) => v,
|
|
16
18
|
}));
|
|
17
19
|
|
|
18
20
|
mock.module('../config/loader.js', () => ({
|
|
@@ -53,37 +55,176 @@ mock.module('../daemon/identity-helpers.js', () => ({
|
|
|
53
55
|
getAssistantName: () => mockAssistantName,
|
|
54
56
|
}));
|
|
55
57
|
|
|
56
|
-
// ── Mock recording-intent — we control the
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// ── Mock recording-intent — we control the resolution result ───────────────
|
|
59
|
+
//
|
|
60
|
+
// Bun's mock.module() is global and persists across test files in the same
|
|
61
|
+
// process (no per-file isolation). To prevent this mock from breaking
|
|
62
|
+
// recording-intent.test.ts (which tests the REAL resolveRecordingIntent),
|
|
63
|
+
// we capture real function references before mocking and use a globalThis
|
|
64
|
+
// flag to conditionally delegate to them. The flag is only true while this
|
|
65
|
+
// file's tests are running; after this file completes (afterAll), the mock
|
|
66
|
+
// transparently delegates to the real implementation.
|
|
67
|
+
|
|
68
|
+
type RecordingIntentResult =
|
|
69
|
+
| { kind: 'none' }
|
|
70
|
+
| { kind: 'start_only' }
|
|
71
|
+
| { kind: 'stop_only' }
|
|
72
|
+
| { kind: 'start_with_remainder'; remainder: string }
|
|
73
|
+
| { kind: 'stop_with_remainder'; remainder: string }
|
|
74
|
+
| { kind: 'start_and_stop_only' }
|
|
75
|
+
| { kind: 'start_and_stop_with_remainder'; remainder: string }
|
|
76
|
+
| { kind: 'restart_only' }
|
|
77
|
+
| { kind: 'restart_with_remainder'; remainder: string }
|
|
78
|
+
| { kind: 'pause_only' }
|
|
79
|
+
| { kind: 'resume_only' };
|
|
80
|
+
|
|
81
|
+
let mockIntentResult: RecordingIntentResult = { kind: 'none' };
|
|
82
|
+
|
|
83
|
+
// Capture real function references BEFORE mock.module replaces the module.
|
|
84
|
+
// require() at this point returns the real module since mock.module has not
|
|
85
|
+
// been called yet for this specifier.
|
|
86
|
+
const _realRecordingIntentMod = require('../daemon/recording-intent.js');
|
|
87
|
+
const _realResolveRecordingIntent = _realRecordingIntentMod.resolveRecordingIntent;
|
|
88
|
+
const _realStripDynamicNames = _realRecordingIntentMod.stripDynamicNames;
|
|
89
|
+
|
|
90
|
+
// Flag: when true, the mock returns controlled test values; when false, it
|
|
91
|
+
// delegates to the real implementation. Starts false so that if the mock
|
|
92
|
+
// bleeds to other test files, those files get the real behavior.
|
|
93
|
+
(globalThis as any).__riHandlerUseMockIntent = false;
|
|
59
94
|
|
|
60
95
|
mock.module('../daemon/recording-intent.js', () => ({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
resolveRecordingIntent: (...args: any[]) => {
|
|
97
|
+
if ((globalThis as any).__riHandlerUseMockIntent) return mockIntentResult;
|
|
98
|
+
return _realResolveRecordingIntent(...args);
|
|
99
|
+
},
|
|
100
|
+
stripDynamicNames: (...args: any[]) => {
|
|
101
|
+
if ((globalThis as any).__riHandlerUseMockIntent) return args[0];
|
|
102
|
+
return _realStripDynamicNames(...args);
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
// ── Mock recording-executor — we control the execution output ──────────────
|
|
107
|
+
//
|
|
108
|
+
// Same transparent-mock pattern as recording-intent above. We try to capture
|
|
109
|
+
// the real exports before mocking; if the require fails (e.g., due to missing
|
|
110
|
+
// transitive dependencies when this file runs in isolation), we fall back to
|
|
111
|
+
// the controlled mock since the real module is not needed in that scenario.
|
|
112
|
+
|
|
113
|
+
interface RecordingExecutionOutput {
|
|
114
|
+
handled: boolean;
|
|
115
|
+
responseText?: string;
|
|
116
|
+
remainderText?: string;
|
|
117
|
+
pendingStart?: boolean;
|
|
118
|
+
pendingStop?: boolean;
|
|
119
|
+
pendingRestart?: boolean;
|
|
120
|
+
recordingStarted?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let mockExecuteResult: RecordingExecutionOutput = { handled: false };
|
|
124
|
+
let executorCalled = false;
|
|
125
|
+
|
|
126
|
+
let _realExecuteRecordingIntent: ((...args: any[]) => any) | null = null;
|
|
127
|
+
try {
|
|
128
|
+
const _mod = require('../daemon/recording-executor.js');
|
|
129
|
+
_realExecuteRecordingIntent = _mod.executeRecordingIntent;
|
|
130
|
+
} catch {
|
|
131
|
+
// Transitive dependency loading may fail when this file runs alone;
|
|
132
|
+
// the controlled mock will be used exclusively in that case.
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
mock.module('../daemon/recording-executor.js', () => ({
|
|
136
|
+
executeRecordingIntent: (...args: any[]) => {
|
|
137
|
+
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
138
|
+
executorCalled = true;
|
|
139
|
+
return mockExecuteResult;
|
|
140
|
+
}
|
|
141
|
+
if (_realExecuteRecordingIntent) return _realExecuteRecordingIntent(...args);
|
|
142
|
+
// Fallback if real function was not captured
|
|
143
|
+
return { handled: false };
|
|
144
|
+
},
|
|
69
145
|
}));
|
|
70
146
|
|
|
71
147
|
// ── Mock recording handlers ────────────────────────────────────────────────
|
|
148
|
+
//
|
|
149
|
+
// Same transparent-mock pattern. The intent test file re-mocks this module
|
|
150
|
+
// inside its own describe block, which will override this mock for those tests.
|
|
151
|
+
// The transparent fallback here ensures that if a third test file imports
|
|
152
|
+
// handlers/recording.js, it gets the real behavior.
|
|
72
153
|
|
|
73
154
|
let recordingStartCalled = false;
|
|
74
155
|
let recordingStopCalled = false;
|
|
156
|
+
let recordingRestartCalled = false;
|
|
157
|
+
let recordingPauseCalled = false;
|
|
158
|
+
let recordingResumeCalled = false;
|
|
159
|
+
|
|
160
|
+
let _realHandleRecordingStart: ((...args: any[]) => any) | null = null;
|
|
161
|
+
let _realHandleRecordingStop: ((...args: any[]) => any) | null = null;
|
|
162
|
+
let _realHandleRecordingRestart: ((...args: any[]) => any) | null = null;
|
|
163
|
+
let _realHandleRecordingPause: ((...args: any[]) => any) | null = null;
|
|
164
|
+
let _realHandleRecordingResume: ((...args: any[]) => any) | null = null;
|
|
165
|
+
let _realIsRecordingIdle: ((...args: any[]) => any) | null = null;
|
|
166
|
+
let _realRecordingHandlers: any = {};
|
|
167
|
+
let _realResetRecordingState: ((...args: any[]) => any) | null = null;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const _mod = require('../daemon/handlers/recording.js');
|
|
171
|
+
_realHandleRecordingStart = _mod.handleRecordingStart;
|
|
172
|
+
_realHandleRecordingStop = _mod.handleRecordingStop;
|
|
173
|
+
_realHandleRecordingRestart = _mod.handleRecordingRestart;
|
|
174
|
+
_realHandleRecordingPause = _mod.handleRecordingPause;
|
|
175
|
+
_realHandleRecordingResume = _mod.handleRecordingResume;
|
|
176
|
+
_realIsRecordingIdle = _mod.isRecordingIdle;
|
|
177
|
+
_realRecordingHandlers = _mod.recordingHandlers ?? {};
|
|
178
|
+
_realResetRecordingState = _mod.__resetRecordingState;
|
|
179
|
+
} catch {
|
|
180
|
+
// Same as above — controlled mock will be used exclusively.
|
|
181
|
+
}
|
|
75
182
|
|
|
76
183
|
mock.module('../daemon/handlers/recording.js', () => ({
|
|
77
|
-
handleRecordingStart: () => {
|
|
78
|
-
|
|
79
|
-
|
|
184
|
+
handleRecordingStart: (...args: any[]) => {
|
|
185
|
+
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
186
|
+
recordingStartCalled = true;
|
|
187
|
+
return 'mock-recording-id';
|
|
188
|
+
}
|
|
189
|
+
return _realHandleRecordingStart?.(...args);
|
|
190
|
+
},
|
|
191
|
+
handleRecordingStop: (...args: any[]) => {
|
|
192
|
+
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
193
|
+
recordingStopCalled = true;
|
|
194
|
+
return 'mock-recording-id';
|
|
195
|
+
}
|
|
196
|
+
return _realHandleRecordingStop?.(...args);
|
|
197
|
+
},
|
|
198
|
+
handleRecordingRestart: (...args: any[]) => {
|
|
199
|
+
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
200
|
+
recordingRestartCalled = true;
|
|
201
|
+
return { initiated: true, responseText: 'Restarting screen recording.', operationToken: 'mock-token' };
|
|
202
|
+
}
|
|
203
|
+
return _realHandleRecordingRestart?.(...args);
|
|
80
204
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
205
|
+
handleRecordingPause: (...args: any[]) => {
|
|
206
|
+
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
207
|
+
recordingPauseCalled = true;
|
|
208
|
+
return 'mock-recording-id';
|
|
209
|
+
}
|
|
210
|
+
return _realHandleRecordingPause?.(...args);
|
|
211
|
+
},
|
|
212
|
+
handleRecordingResume: (...args: any[]) => {
|
|
213
|
+
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
214
|
+
recordingResumeCalled = true;
|
|
215
|
+
return 'mock-recording-id';
|
|
216
|
+
}
|
|
217
|
+
return _realHandleRecordingResume?.(...args);
|
|
218
|
+
},
|
|
219
|
+
isRecordingIdle: (...args: any[]) => {
|
|
220
|
+
if ((globalThis as any).__riHandlerUseMockIntent) return true;
|
|
221
|
+
return _realIsRecordingIdle?.(...args) ?? true;
|
|
222
|
+
},
|
|
223
|
+
recordingHandlers: _realRecordingHandlers,
|
|
224
|
+
__resetRecordingState: (...args: any[]) => {
|
|
225
|
+
if ((globalThis as any).__riHandlerUseMockIntent) return;
|
|
226
|
+
return _realResetRecordingState?.(...args);
|
|
84
227
|
},
|
|
85
|
-
recordingHandlers: {},
|
|
86
|
-
__resetRecordingState: noop,
|
|
87
228
|
}));
|
|
88
229
|
|
|
89
230
|
// ── Mock conversation store ────────────────────────────────────────────────
|
|
@@ -91,7 +232,10 @@ mock.module('../daemon/handlers/recording.js', () => ({
|
|
|
91
232
|
mock.module('../memory/conversation-store.js', () => ({
|
|
92
233
|
getMessages: () => [],
|
|
93
234
|
addMessage: () => ({ id: 'msg-mock', role: 'assistant', content: '' }),
|
|
94
|
-
createConversation: (
|
|
235
|
+
createConversation: (titleOrOpts?: string | { title?: string }) => {
|
|
236
|
+
const title = typeof titleOrOpts === 'string' ? titleOrOpts : titleOrOpts?.title ?? 'Untitled';
|
|
237
|
+
return { id: 'conv-mock', title };
|
|
238
|
+
},
|
|
95
239
|
getConversation: () => ({ id: 'conv-mock' }),
|
|
96
240
|
updateConversationTitle: noop,
|
|
97
241
|
clearAll: noop,
|
|
@@ -102,7 +246,7 @@ mock.module('../memory/conversation-store.js', () => ({
|
|
|
102
246
|
}));
|
|
103
247
|
|
|
104
248
|
mock.module('../memory/conversation-title-service.js', () => ({
|
|
105
|
-
GENERATING_TITLE: '(generating
|
|
249
|
+
GENERATING_TITLE: '(generating\u2026)',
|
|
106
250
|
queueGenerateConversationTitle: noop,
|
|
107
251
|
UNTITLED_FALLBACK: 'Untitled',
|
|
108
252
|
}));
|
|
@@ -120,6 +264,10 @@ mock.module('../security/secret-ingress.js', () => ({
|
|
|
120
264
|
checkIngressForSecrets: () => ({ blocked: false }),
|
|
121
265
|
}));
|
|
122
266
|
|
|
267
|
+
mock.module('../security/secret-scanner.js', () => ({
|
|
268
|
+
redactSecrets: (text: string) => text,
|
|
269
|
+
}));
|
|
270
|
+
|
|
123
271
|
// ── Mock classifier (for task_submit fallthrough) ──────────────────────────
|
|
124
272
|
|
|
125
273
|
let classifierCalled = false;
|
|
@@ -147,6 +295,9 @@ mock.module('../daemon/handlers/computer-use.js', () => ({
|
|
|
147
295
|
|
|
148
296
|
mock.module('../providers/provider-send-message.js', () => ({
|
|
149
297
|
getConfiguredProvider: () => null,
|
|
298
|
+
extractText: (_response: unknown) => '',
|
|
299
|
+
createTimeout: (_ms: number) => ({ signal: new AbortController().signal, cleanup: () => {} }),
|
|
300
|
+
userMessage: (text: string) => ({ role: 'user', content: text }),
|
|
150
301
|
}));
|
|
151
302
|
|
|
152
303
|
// ── Mock external conversation store ───────────────────────────────────────
|
|
@@ -228,6 +379,10 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
228
379
|
getQueueDepth: () => 0,
|
|
229
380
|
setPreactivatedSkillIds: noop,
|
|
230
381
|
redirectToSecurePrompt: noop,
|
|
382
|
+
setEscalationHandler: noop,
|
|
383
|
+
dispose: noop,
|
|
384
|
+
hasPendingConfirmation: () => false,
|
|
385
|
+
hasPendingSecret: () => false,
|
|
231
386
|
};
|
|
232
387
|
|
|
233
388
|
const sessions = new Map<string, any>();
|
|
@@ -258,19 +413,35 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
258
413
|
return { ctx, sent, fakeSocket };
|
|
259
414
|
}
|
|
260
415
|
|
|
416
|
+
function resetMockState(): void {
|
|
417
|
+
// Enable mock mode for this file's tests
|
|
418
|
+
(globalThis as any).__riHandlerUseMockIntent = true;
|
|
419
|
+
mockIntentResult = { kind: 'none' };
|
|
420
|
+
mockExecuteResult = { handled: false };
|
|
421
|
+
mockAssistantName = null;
|
|
422
|
+
recordingStartCalled = false;
|
|
423
|
+
recordingStopCalled = false;
|
|
424
|
+
recordingRestartCalled = false;
|
|
425
|
+
recordingPauseCalled = false;
|
|
426
|
+
recordingResumeCalled = false;
|
|
427
|
+
executorCalled = false;
|
|
428
|
+
classifierCalled = false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Disable mock mode after all tests in this file complete, so that if the
|
|
432
|
+
// mock bleeds to other test files they get the real implementation.
|
|
433
|
+
afterAll(() => {
|
|
434
|
+
(globalThis as any).__riHandlerUseMockIntent = false;
|
|
435
|
+
});
|
|
436
|
+
|
|
261
437
|
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
262
438
|
|
|
263
439
|
describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
264
|
-
beforeEach(
|
|
265
|
-
mockClassifyResult = 'none';
|
|
266
|
-
mockAssistantName = null;
|
|
267
|
-
recordingStartCalled = false;
|
|
268
|
-
recordingStopCalled = false;
|
|
269
|
-
classifierCalled = false;
|
|
270
|
-
});
|
|
440
|
+
beforeEach(resetMockState);
|
|
271
441
|
|
|
272
|
-
test('start_only →
|
|
273
|
-
|
|
442
|
+
test('start_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early', async () => {
|
|
443
|
+
mockIntentResult = { kind: 'start_only' };
|
|
444
|
+
mockExecuteResult = { handled: true, responseText: 'Starting screen recording.', recordingStarted: true };
|
|
274
445
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
275
446
|
|
|
276
447
|
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
@@ -280,18 +451,21 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
280
451
|
ctx,
|
|
281
452
|
);
|
|
282
453
|
|
|
283
|
-
expect(
|
|
284
|
-
expect(recordingStopCalled).toBe(false);
|
|
454
|
+
expect(executorCalled).toBe(true);
|
|
285
455
|
expect(classifierCalled).toBe(false);
|
|
286
456
|
|
|
287
457
|
const types = sent.map((m) => m.type);
|
|
288
458
|
expect(types).toContain('task_routed');
|
|
289
459
|
expect(types).toContain('assistant_text_delta');
|
|
290
460
|
expect(types).toContain('message_complete');
|
|
461
|
+
|
|
462
|
+
const textDelta = sent.find((m) => m.type === 'assistant_text_delta');
|
|
463
|
+
expect(textDelta?.text).toBe('Starting screen recording.');
|
|
291
464
|
});
|
|
292
465
|
|
|
293
|
-
test('stop_only →
|
|
294
|
-
|
|
466
|
+
test('stop_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early', async () => {
|
|
467
|
+
mockIntentResult = { kind: 'stop_only' };
|
|
468
|
+
mockExecuteResult = { handled: true, responseText: 'Stopping the recording.' };
|
|
295
469
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
296
470
|
|
|
297
471
|
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
@@ -301,18 +475,21 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
301
475
|
ctx,
|
|
302
476
|
);
|
|
303
477
|
|
|
304
|
-
expect(
|
|
305
|
-
expect(recordingStartCalled).toBe(false);
|
|
478
|
+
expect(executorCalled).toBe(true);
|
|
306
479
|
expect(classifierCalled).toBe(false);
|
|
307
480
|
|
|
308
481
|
const types = sent.map((m) => m.type);
|
|
309
482
|
expect(types).toContain('task_routed');
|
|
310
483
|
expect(types).toContain('assistant_text_delta');
|
|
311
484
|
expect(types).toContain('message_complete');
|
|
485
|
+
|
|
486
|
+
const textDelta = sent.find((m) => m.type === 'assistant_text_delta');
|
|
487
|
+
expect(textDelta?.text).toBe('Stopping the recording.');
|
|
312
488
|
});
|
|
313
489
|
|
|
314
|
-
test('
|
|
315
|
-
|
|
490
|
+
test('start_with_remainder → defers recording, falls through to classifier with remaining text', async () => {
|
|
491
|
+
mockIntentResult = { kind: 'start_with_remainder', remainder: 'open Safari' };
|
|
492
|
+
mockExecuteResult = { handled: false, remainderText: 'open Safari', pendingStart: true };
|
|
316
493
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
317
494
|
|
|
318
495
|
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
@@ -322,11 +499,9 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
322
499
|
ctx,
|
|
323
500
|
);
|
|
324
501
|
|
|
325
|
-
expect(recordingStartCalled).toBe(false);
|
|
326
|
-
expect(recordingStopCalled).toBe(false);
|
|
327
502
|
expect(classifierCalled).toBe(true);
|
|
328
503
|
|
|
329
|
-
// Should NOT have recording-
|
|
504
|
+
// Should NOT have recording-only messages before the classifier output
|
|
330
505
|
const recordingSpecific = sent.filter(
|
|
331
506
|
(m) => m.type === 'assistant_text_delta' && typeof m.text === 'string' &&
|
|
332
507
|
(m.text.includes('Starting screen recording') || m.text.includes('Stopping the recording')),
|
|
@@ -334,8 +509,8 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
334
509
|
expect(recordingSpecific).toHaveLength(0);
|
|
335
510
|
});
|
|
336
511
|
|
|
337
|
-
test('none → does NOT call
|
|
338
|
-
|
|
512
|
+
test('none → does NOT call executeRecordingIntent, falls through to classifier', async () => {
|
|
513
|
+
mockIntentResult = { kind: 'none' };
|
|
339
514
|
const { ctx, sent: _sent, fakeSocket } = createCtx();
|
|
340
515
|
|
|
341
516
|
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
@@ -345,23 +520,187 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
345
520
|
ctx,
|
|
346
521
|
);
|
|
347
522
|
|
|
348
|
-
expect(
|
|
349
|
-
expect(
|
|
523
|
+
expect(executorCalled).toBe(false);
|
|
524
|
+
expect(classifierCalled).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('restart_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early', async () => {
|
|
528
|
+
mockIntentResult = { kind: 'restart_only' };
|
|
529
|
+
mockExecuteResult = { handled: true, responseText: 'Restarting screen recording.' };
|
|
530
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
531
|
+
|
|
532
|
+
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
533
|
+
await handleTaskSubmit(
|
|
534
|
+
{ type: 'task_submit', task: 'restart the recording', source: 'voice' } as any,
|
|
535
|
+
fakeSocket,
|
|
536
|
+
ctx,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(executorCalled).toBe(true);
|
|
540
|
+
expect(classifierCalled).toBe(false);
|
|
541
|
+
|
|
542
|
+
const types = sent.map((m) => m.type);
|
|
543
|
+
expect(types).toContain('task_routed');
|
|
544
|
+
expect(types).toContain('assistant_text_delta');
|
|
545
|
+
expect(types).toContain('message_complete');
|
|
546
|
+
|
|
547
|
+
const textDelta = sent.find((m) => m.type === 'assistant_text_delta');
|
|
548
|
+
expect(textDelta?.text).toBe('Restarting screen recording.');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('pause_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early', async () => {
|
|
552
|
+
mockIntentResult = { kind: 'pause_only' };
|
|
553
|
+
mockExecuteResult = { handled: true, responseText: 'Pausing the recording.' };
|
|
554
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
555
|
+
|
|
556
|
+
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
557
|
+
await handleTaskSubmit(
|
|
558
|
+
{ type: 'task_submit', task: 'pause the recording', source: 'voice' } as any,
|
|
559
|
+
fakeSocket,
|
|
560
|
+
ctx,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
expect(executorCalled).toBe(true);
|
|
564
|
+
expect(classifierCalled).toBe(false);
|
|
565
|
+
|
|
566
|
+
const types = sent.map((m) => m.type);
|
|
567
|
+
expect(types).toContain('task_routed');
|
|
568
|
+
expect(types).toContain('assistant_text_delta');
|
|
569
|
+
expect(types).toContain('message_complete');
|
|
570
|
+
|
|
571
|
+
const textDelta = sent.find((m) => m.type === 'assistant_text_delta');
|
|
572
|
+
expect(textDelta?.text).toBe('Pausing the recording.');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test('resume_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early', async () => {
|
|
576
|
+
mockIntentResult = { kind: 'resume_only' };
|
|
577
|
+
mockExecuteResult = { handled: true, responseText: 'Resuming the recording.' };
|
|
578
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
579
|
+
|
|
580
|
+
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
581
|
+
await handleTaskSubmit(
|
|
582
|
+
{ type: 'task_submit', task: 'resume the recording', source: 'voice' } as any,
|
|
583
|
+
fakeSocket,
|
|
584
|
+
ctx,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
expect(executorCalled).toBe(true);
|
|
588
|
+
expect(classifierCalled).toBe(false);
|
|
589
|
+
|
|
590
|
+
const types = sent.map((m) => m.type);
|
|
591
|
+
expect(types).toContain('task_routed');
|
|
592
|
+
expect(types).toContain('assistant_text_delta');
|
|
593
|
+
expect(types).toContain('message_complete');
|
|
594
|
+
|
|
595
|
+
const textDelta = sent.find((m) => m.type === 'assistant_text_delta');
|
|
596
|
+
expect(textDelta?.text).toBe('Resuming the recording.');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('restart_with_remainder → defers restart, falls through to classifier with remaining text', async () => {
|
|
600
|
+
mockIntentResult = { kind: 'restart_with_remainder', remainder: 'open Safari' };
|
|
601
|
+
mockExecuteResult = { handled: false, remainderText: 'open Safari', pendingRestart: true };
|
|
602
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
603
|
+
|
|
604
|
+
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
605
|
+
await handleTaskSubmit(
|
|
606
|
+
{ type: 'task_submit', task: 'restart the recording and open Safari', source: 'voice' } as any,
|
|
607
|
+
fakeSocket,
|
|
608
|
+
ctx,
|
|
609
|
+
);
|
|
610
|
+
|
|
350
611
|
expect(classifierCalled).toBe(true);
|
|
612
|
+
|
|
613
|
+
// Should NOT have restart-specific messages before classifier output
|
|
614
|
+
const recordingSpecific = sent.filter(
|
|
615
|
+
(m) => m.type === 'assistant_text_delta' && typeof m.text === 'string' &&
|
|
616
|
+
m.text.includes('Restarting screen recording'),
|
|
617
|
+
);
|
|
618
|
+
expect(recordingSpecific).toHaveLength(0);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('commandIntent restart → routes directly via handleRecordingRestart, returns early', async () => {
|
|
622
|
+
// commandIntent bypasses text-based intent resolution entirely
|
|
623
|
+
mockIntentResult = { kind: 'none' }; // should not matter
|
|
624
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
625
|
+
|
|
626
|
+
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
627
|
+
await handleTaskSubmit(
|
|
628
|
+
{
|
|
629
|
+
type: 'task_submit',
|
|
630
|
+
task: 'restart recording',
|
|
631
|
+
source: 'voice',
|
|
632
|
+
commandIntent: { domain: 'screen_recording', action: 'restart' },
|
|
633
|
+
} as any,
|
|
634
|
+
fakeSocket,
|
|
635
|
+
ctx,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
expect(recordingRestartCalled).toBe(true);
|
|
639
|
+
expect(classifierCalled).toBe(false);
|
|
640
|
+
|
|
641
|
+
const types = sent.map((m) => m.type);
|
|
642
|
+
expect(types).toContain('task_routed');
|
|
643
|
+
expect(types).toContain('assistant_text_delta');
|
|
644
|
+
expect(types).toContain('message_complete');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test('commandIntent pause → routes directly via handleRecordingPause, returns early', async () => {
|
|
648
|
+
mockIntentResult = { kind: 'none' };
|
|
649
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
650
|
+
|
|
651
|
+
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
652
|
+
await handleTaskSubmit(
|
|
653
|
+
{
|
|
654
|
+
type: 'task_submit',
|
|
655
|
+
task: 'pause recording',
|
|
656
|
+
source: 'voice',
|
|
657
|
+
commandIntent: { domain: 'screen_recording', action: 'pause' },
|
|
658
|
+
} as any,
|
|
659
|
+
fakeSocket,
|
|
660
|
+
ctx,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(recordingPauseCalled).toBe(true);
|
|
664
|
+
expect(classifierCalled).toBe(false);
|
|
665
|
+
|
|
666
|
+
const types = sent.map((m) => m.type);
|
|
667
|
+
expect(types).toContain('task_routed');
|
|
668
|
+
expect(types).toContain('assistant_text_delta');
|
|
669
|
+
expect(types).toContain('message_complete');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('commandIntent resume → routes directly via handleRecordingResume, returns early', async () => {
|
|
673
|
+
mockIntentResult = { kind: 'none' };
|
|
674
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
675
|
+
|
|
676
|
+
const { handleTaskSubmit } = await import('../daemon/handlers/misc.js');
|
|
677
|
+
await handleTaskSubmit(
|
|
678
|
+
{
|
|
679
|
+
type: 'task_submit',
|
|
680
|
+
task: 'resume recording',
|
|
681
|
+
source: 'voice',
|
|
682
|
+
commandIntent: { domain: 'screen_recording', action: 'resume' },
|
|
683
|
+
} as any,
|
|
684
|
+
fakeSocket,
|
|
685
|
+
ctx,
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
expect(recordingResumeCalled).toBe(true);
|
|
689
|
+
expect(classifierCalled).toBe(false);
|
|
690
|
+
|
|
691
|
+
const types = sent.map((m) => m.type);
|
|
692
|
+
expect(types).toContain('task_routed');
|
|
693
|
+
expect(types).toContain('assistant_text_delta');
|
|
694
|
+
expect(types).toContain('message_complete');
|
|
351
695
|
});
|
|
352
696
|
});
|
|
353
697
|
|
|
354
698
|
describe('recording intent handler integration — handleUserMessage', () => {
|
|
355
|
-
beforeEach(
|
|
356
|
-
mockClassifyResult = 'none';
|
|
357
|
-
mockAssistantName = null;
|
|
358
|
-
recordingStartCalled = false;
|
|
359
|
-
recordingStopCalled = false;
|
|
360
|
-
classifierCalled = false;
|
|
361
|
-
});
|
|
699
|
+
beforeEach(resetMockState);
|
|
362
700
|
|
|
363
|
-
test('start_only →
|
|
364
|
-
|
|
701
|
+
test('start_only → executeRecordingIntent called, sends text_delta + message_complete, returns early', async () => {
|
|
702
|
+
mockIntentResult = { kind: 'start_only' };
|
|
703
|
+
mockExecuteResult = { handled: true, responseText: 'Starting screen recording.', recordingStarted: true };
|
|
365
704
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
366
705
|
|
|
367
706
|
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
@@ -376,22 +715,20 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
376
715
|
ctx,
|
|
377
716
|
);
|
|
378
717
|
|
|
379
|
-
expect(
|
|
380
|
-
expect(recordingStopCalled).toBe(false);
|
|
718
|
+
expect(executorCalled).toBe(true);
|
|
381
719
|
|
|
382
720
|
const types = sent.map((m) => m.type);
|
|
383
721
|
expect(types).toContain('assistant_text_delta');
|
|
384
722
|
expect(types).toContain('message_complete');
|
|
385
723
|
|
|
386
|
-
//
|
|
387
|
-
// The absence of enqueueMessage side effects is hard to test directly,
|
|
388
|
-
// but we verify message_complete was the last message sent.
|
|
724
|
+
// message_complete should be the last message sent (recording returned early)
|
|
389
725
|
const lastMsg = sent[sent.length - 1];
|
|
390
726
|
expect(lastMsg.type).toBe('message_complete');
|
|
391
727
|
});
|
|
392
728
|
|
|
393
|
-
test('stop_only →
|
|
394
|
-
|
|
729
|
+
test('stop_only → executeRecordingIntent called, sends text_delta + message_complete, returns early', async () => {
|
|
730
|
+
mockIntentResult = { kind: 'stop_only' };
|
|
731
|
+
mockExecuteResult = { handled: true, responseText: 'Stopping the recording.' };
|
|
395
732
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
396
733
|
|
|
397
734
|
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
@@ -406,8 +743,7 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
406
743
|
ctx,
|
|
407
744
|
);
|
|
408
745
|
|
|
409
|
-
expect(
|
|
410
|
-
expect(recordingStartCalled).toBe(false);
|
|
746
|
+
expect(executorCalled).toBe(true);
|
|
411
747
|
|
|
412
748
|
const types = sent.map((m) => m.type);
|
|
413
749
|
expect(types).toContain('assistant_text_delta');
|
|
@@ -417,8 +753,9 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
417
753
|
expect(lastMsg.type).toBe('message_complete');
|
|
418
754
|
});
|
|
419
755
|
|
|
420
|
-
test('
|
|
421
|
-
|
|
756
|
+
test('start_with_remainder → does NOT return early, proceeds to normal message processing', async () => {
|
|
757
|
+
mockIntentResult = { kind: 'start_with_remainder', remainder: 'open Safari' };
|
|
758
|
+
mockExecuteResult = { handled: false, remainderText: 'open Safari', pendingStart: true };
|
|
422
759
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
423
760
|
|
|
424
761
|
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
@@ -433,10 +770,10 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
433
770
|
ctx,
|
|
434
771
|
);
|
|
435
772
|
|
|
436
|
-
|
|
437
|
-
expect(
|
|
773
|
+
// Should deferred recording start and proceed to normal processing
|
|
774
|
+
expect(recordingStartCalled).toBe(true);
|
|
438
775
|
|
|
439
|
-
// Should NOT have recording-specific messages
|
|
776
|
+
// Should NOT have recording-specific intercept messages
|
|
440
777
|
const recordingSpecific = sent.filter(
|
|
441
778
|
(m) => m.type === 'assistant_text_delta' && typeof m.text === 'string' &&
|
|
442
779
|
(m.text.includes('Starting screen recording') || m.text.includes('Stopping the recording')),
|
|
@@ -445,7 +782,7 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
445
782
|
});
|
|
446
783
|
|
|
447
784
|
test('none → does NOT intercept, proceeds to normal message processing', async () => {
|
|
448
|
-
|
|
785
|
+
mockIntentResult = { kind: 'none' };
|
|
449
786
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
450
787
|
|
|
451
788
|
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
@@ -460,8 +797,7 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
460
797
|
ctx,
|
|
461
798
|
);
|
|
462
799
|
|
|
463
|
-
expect(
|
|
464
|
-
expect(recordingStopCalled).toBe(false);
|
|
800
|
+
expect(executorCalled).toBe(false);
|
|
465
801
|
|
|
466
802
|
// Should NOT have recording-specific messages
|
|
467
803
|
const recordingSpecific = sent.filter(
|
|
@@ -470,4 +806,188 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
470
806
|
);
|
|
471
807
|
expect(recordingSpecific).toHaveLength(0);
|
|
472
808
|
});
|
|
809
|
+
|
|
810
|
+
test('restart_only → executeRecordingIntent called, sends text_delta + message_complete, returns early', async () => {
|
|
811
|
+
mockIntentResult = { kind: 'restart_only' };
|
|
812
|
+
mockExecuteResult = { handled: true, responseText: 'Restarting screen recording.' };
|
|
813
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
814
|
+
|
|
815
|
+
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
816
|
+
await handleUserMessage(
|
|
817
|
+
{
|
|
818
|
+
type: 'user_message',
|
|
819
|
+
sessionId: 'test-session',
|
|
820
|
+
content: 'restart the recording',
|
|
821
|
+
interface: 'vellum',
|
|
822
|
+
} as any,
|
|
823
|
+
fakeSocket,
|
|
824
|
+
ctx,
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
expect(executorCalled).toBe(true);
|
|
828
|
+
|
|
829
|
+
const types = sent.map((m) => m.type);
|
|
830
|
+
expect(types).toContain('assistant_text_delta');
|
|
831
|
+
expect(types).toContain('message_complete');
|
|
832
|
+
|
|
833
|
+
const lastMsg = sent[sent.length - 1];
|
|
834
|
+
expect(lastMsg.type).toBe('message_complete');
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test('pause_only → executeRecordingIntent called, sends text_delta + message_complete, returns early', async () => {
|
|
838
|
+
mockIntentResult = { kind: 'pause_only' };
|
|
839
|
+
mockExecuteResult = { handled: true, responseText: 'Pausing the recording.' };
|
|
840
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
841
|
+
|
|
842
|
+
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
843
|
+
await handleUserMessage(
|
|
844
|
+
{
|
|
845
|
+
type: 'user_message',
|
|
846
|
+
sessionId: 'test-session',
|
|
847
|
+
content: 'pause the recording',
|
|
848
|
+
interface: 'vellum',
|
|
849
|
+
} as any,
|
|
850
|
+
fakeSocket,
|
|
851
|
+
ctx,
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
expect(executorCalled).toBe(true);
|
|
855
|
+
|
|
856
|
+
const types = sent.map((m) => m.type);
|
|
857
|
+
expect(types).toContain('assistant_text_delta');
|
|
858
|
+
expect(types).toContain('message_complete');
|
|
859
|
+
|
|
860
|
+
const lastMsg = sent[sent.length - 1];
|
|
861
|
+
expect(lastMsg.type).toBe('message_complete');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('resume_only → executeRecordingIntent called, sends text_delta + message_complete, returns early', async () => {
|
|
865
|
+
mockIntentResult = { kind: 'resume_only' };
|
|
866
|
+
mockExecuteResult = { handled: true, responseText: 'Resuming the recording.' };
|
|
867
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
868
|
+
|
|
869
|
+
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
870
|
+
await handleUserMessage(
|
|
871
|
+
{
|
|
872
|
+
type: 'user_message',
|
|
873
|
+
sessionId: 'test-session',
|
|
874
|
+
content: 'resume the recording',
|
|
875
|
+
interface: 'vellum',
|
|
876
|
+
} as any,
|
|
877
|
+
fakeSocket,
|
|
878
|
+
ctx,
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
expect(executorCalled).toBe(true);
|
|
882
|
+
|
|
883
|
+
const types = sent.map((m) => m.type);
|
|
884
|
+
expect(types).toContain('assistant_text_delta');
|
|
885
|
+
expect(types).toContain('message_complete');
|
|
886
|
+
|
|
887
|
+
const lastMsg = sent[sent.length - 1];
|
|
888
|
+
expect(lastMsg.type).toBe('message_complete');
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test('restart_with_remainder → defers restart, continues with remaining text', async () => {
|
|
892
|
+
mockIntentResult = { kind: 'restart_with_remainder', remainder: 'open Safari' };
|
|
893
|
+
mockExecuteResult = { handled: false, remainderText: 'open Safari', pendingRestart: true };
|
|
894
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
895
|
+
|
|
896
|
+
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
897
|
+
await handleUserMessage(
|
|
898
|
+
{
|
|
899
|
+
type: 'user_message',
|
|
900
|
+
sessionId: 'test-session',
|
|
901
|
+
content: 'restart the recording and open Safari',
|
|
902
|
+
interface: 'vellum',
|
|
903
|
+
} as any,
|
|
904
|
+
fakeSocket,
|
|
905
|
+
ctx,
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// Deferred restart should have been executed
|
|
909
|
+
expect(recordingRestartCalled).toBe(true);
|
|
910
|
+
|
|
911
|
+
// Should NOT have restart-specific intercept messages
|
|
912
|
+
const recordingSpecific = sent.filter(
|
|
913
|
+
(m) => m.type === 'assistant_text_delta' && typeof m.text === 'string' &&
|
|
914
|
+
m.text.includes('Restarting screen recording'),
|
|
915
|
+
);
|
|
916
|
+
expect(recordingSpecific).toHaveLength(0);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test('commandIntent restart → routes directly via handleRecordingRestart, returns early', async () => {
|
|
920
|
+
mockIntentResult = { kind: 'none' };
|
|
921
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
922
|
+
|
|
923
|
+
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
924
|
+
await handleUserMessage(
|
|
925
|
+
{
|
|
926
|
+
type: 'user_message',
|
|
927
|
+
sessionId: 'test-session',
|
|
928
|
+
content: 'restart recording',
|
|
929
|
+
interface: 'vellum',
|
|
930
|
+
commandIntent: { domain: 'screen_recording', action: 'restart' },
|
|
931
|
+
} as any,
|
|
932
|
+
fakeSocket,
|
|
933
|
+
ctx,
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
expect(recordingRestartCalled).toBe(true);
|
|
937
|
+
|
|
938
|
+
const types = sent.map((m) => m.type);
|
|
939
|
+
expect(types).toContain('assistant_text_delta');
|
|
940
|
+
expect(types).toContain('message_complete');
|
|
941
|
+
|
|
942
|
+
const lastMsg = sent[sent.length - 1];
|
|
943
|
+
expect(lastMsg.type).toBe('message_complete');
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test('commandIntent pause → routes directly via handleRecordingPause, returns early', async () => {
|
|
947
|
+
mockIntentResult = { kind: 'none' };
|
|
948
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
949
|
+
|
|
950
|
+
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
951
|
+
await handleUserMessage(
|
|
952
|
+
{
|
|
953
|
+
type: 'user_message',
|
|
954
|
+
sessionId: 'test-session',
|
|
955
|
+
content: 'pause recording',
|
|
956
|
+
interface: 'vellum',
|
|
957
|
+
commandIntent: { domain: 'screen_recording', action: 'pause' },
|
|
958
|
+
} as any,
|
|
959
|
+
fakeSocket,
|
|
960
|
+
ctx,
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
expect(recordingPauseCalled).toBe(true);
|
|
964
|
+
|
|
965
|
+
const types = sent.map((m) => m.type);
|
|
966
|
+
expect(types).toContain('assistant_text_delta');
|
|
967
|
+
expect(types).toContain('message_complete');
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
test('commandIntent resume → routes directly via handleRecordingResume, returns early', async () => {
|
|
971
|
+
mockIntentResult = { kind: 'none' };
|
|
972
|
+
const { ctx, sent, fakeSocket } = createCtx();
|
|
973
|
+
|
|
974
|
+
const { handleUserMessage } = await import('../daemon/handlers/sessions.js');
|
|
975
|
+
await handleUserMessage(
|
|
976
|
+
{
|
|
977
|
+
type: 'user_message',
|
|
978
|
+
sessionId: 'test-session',
|
|
979
|
+
content: 'resume recording',
|
|
980
|
+
interface: 'vellum',
|
|
981
|
+
commandIntent: { domain: 'screen_recording', action: 'resume' },
|
|
982
|
+
} as any,
|
|
983
|
+
fakeSocket,
|
|
984
|
+
ctx,
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
expect(recordingResumeCalled).toBe(true);
|
|
988
|
+
|
|
989
|
+
const types = sent.map((m) => m.type);
|
|
990
|
+
expect(types).toContain('assistant_text_delta');
|
|
991
|
+
expect(types).toContain('message_complete');
|
|
992
|
+
});
|
|
473
993
|
});
|