@vellumai/assistant 0.4.15 → 0.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +6 -6
- package/README.md +1 -2
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +383 -254
- package/src/__tests__/call-controller.test.ts +1074 -751
- package/src/__tests__/call-routes-http.test.ts +329 -279
- package/src/__tests__/channel-approval-routes.test.ts +2 -13
- package/src/__tests__/channel-approvals.test.ts +227 -182
- package/src/__tests__/channel-guardian.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
- package/src/__tests__/conversation-routes.test.ts +71 -41
- package/src/__tests__/daemon-server-session-init.test.ts +258 -191
- package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
- package/src/__tests__/extract-email.test.ts +42 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
- package/src/__tests__/gateway-only-guard.test.ts +54 -55
- package/src/__tests__/gmail-integration.test.ts +48 -46
- package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
- package/src/__tests__/guardian-outbound-http.test.ts +334 -208
- package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
- package/src/__tests__/guardian-routing-state.test.ts +257 -209
- package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
- package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
- package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
- package/src/__tests__/ingress-reconcile.test.ts +184 -142
- package/src/__tests__/non-member-access-request.test.ts +291 -247
- package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
- package/src/__tests__/pairing-concurrent.test.ts +78 -0
- package/src/__tests__/recording-intent-handler.test.ts +422 -291
- package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
- package/src/__tests__/runtime-events-sse.test.ts +67 -50
- package/src/__tests__/send-endpoint-busy.test.ts +314 -232
- package/src/__tests__/session-approval-overrides.test.ts +93 -91
- package/src/__tests__/sms-messaging-provider.test.ts +74 -47
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
- package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
- package/src/__tests__/twilio-config.test.ts +49 -41
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
- package/src/__tests__/twilio-routes.test.ts +389 -280
- package/src/calls/call-controller.ts +1 -1
- package/src/calls/guardian-action-sweep.ts +6 -6
- package/src/calls/twilio-routes.ts +2 -4
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -4
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +69 -4
- package/src/config/env.ts +39 -29
- package/src/daemon/handlers/config-inbox.ts +5 -5
- package/src/daemon/handlers/skills.ts +18 -10
- package/src/daemon/ipc-contract/messages.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +7 -1
- package/src/daemon/pairing-store.ts +15 -2
- package/src/daemon/session-agent-loop-handlers.ts +5 -0
- package/src/daemon/session-agent-loop.ts +1 -1
- package/src/daemon/session-process.ts +1 -1
- package/src/daemon/session-slash.ts +4 -4
- package/src/daemon/session-surfaces.ts +42 -2
- package/src/runtime/auth/token-service.ts +95 -45
- package/src/runtime/channel-retry-sweep.ts +2 -2
- package/src/runtime/http-server.ts +8 -7
- package/src/runtime/http-types.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +1 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +3 -2
- package/src/runtime/routes/guardian-expiry-sweep.ts +5 -5
- package/src/runtime/routes/pairing-routes.ts +4 -1
- package/src/sequence/reply-matcher.ts +14 -4
- package/src/skills/frontmatter.ts +9 -6
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/util/platform.ts +0 -12
- package/docs/architecture/http-token-refresh.md +0 -274
|
@@ -1,30 +1,36 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
1
2
|
|
|
2
|
-
import
|
|
3
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
|
|
5
6
|
|
|
6
7
|
// ─── Mocks (must be before any imports that depend on them) ─────────────────
|
|
7
8
|
|
|
8
9
|
const noop = () => {};
|
|
9
10
|
const noopLogger = {
|
|
10
|
-
info: noop,
|
|
11
|
+
info: noop,
|
|
12
|
+
warn: noop,
|
|
13
|
+
error: noop,
|
|
14
|
+
debug: noop,
|
|
15
|
+
trace: noop,
|
|
16
|
+
fatal: noop,
|
|
11
17
|
child: () => noopLogger,
|
|
12
18
|
};
|
|
13
19
|
|
|
14
|
-
mock.module(
|
|
20
|
+
mock.module("../util/logger.js", () => ({
|
|
15
21
|
getLogger: () => noopLogger,
|
|
16
22
|
isDebug: () => false,
|
|
17
23
|
truncateForLog: (v: string) => v,
|
|
18
24
|
}));
|
|
19
25
|
|
|
20
|
-
mock.module(
|
|
26
|
+
mock.module("../config/loader.js", () => ({
|
|
21
27
|
getConfig: () => ({
|
|
22
28
|
ui: {},
|
|
23
|
-
|
|
29
|
+
|
|
24
30
|
daemon: { standaloneRecording: true },
|
|
25
|
-
provider:
|
|
26
|
-
model:
|
|
27
|
-
permissions: { mode:
|
|
31
|
+
provider: "mock-provider",
|
|
32
|
+
model: "mock-model",
|
|
33
|
+
permissions: { mode: "legacy" },
|
|
28
34
|
apiKeys: {},
|
|
29
35
|
sandbox: { enabled: false },
|
|
30
36
|
timeouts: { toolExecutionTimeoutSec: 30, permissionTimeoutSec: 5 },
|
|
@@ -53,7 +59,7 @@ mock.module('../config/loader.js', () => ({
|
|
|
53
59
|
|
|
54
60
|
let mockAssistantName: string | null = null;
|
|
55
61
|
|
|
56
|
-
mock.module(
|
|
62
|
+
mock.module("../daemon/identity-helpers.js", () => ({
|
|
57
63
|
getAssistantName: () => mockAssistantName,
|
|
58
64
|
}));
|
|
59
65
|
|
|
@@ -68,26 +74,27 @@ mock.module('../daemon/identity-helpers.js', () => ({
|
|
|
68
74
|
// transparently delegates to the real implementation.
|
|
69
75
|
|
|
70
76
|
type RecordingIntentResult =
|
|
71
|
-
| { kind:
|
|
72
|
-
| { kind:
|
|
73
|
-
| { kind:
|
|
74
|
-
| { kind:
|
|
75
|
-
| { kind:
|
|
76
|
-
| { kind:
|
|
77
|
-
| { kind:
|
|
78
|
-
| { kind:
|
|
79
|
-
| { kind:
|
|
80
|
-
| { kind:
|
|
81
|
-
| { kind:
|
|
82
|
-
|
|
83
|
-
let mockIntentResult: RecordingIntentResult = { kind:
|
|
77
|
+
| { kind: "none" }
|
|
78
|
+
| { kind: "start_only" }
|
|
79
|
+
| { kind: "stop_only" }
|
|
80
|
+
| { kind: "start_with_remainder"; remainder: string }
|
|
81
|
+
| { kind: "stop_with_remainder"; remainder: string }
|
|
82
|
+
| { kind: "start_and_stop_only" }
|
|
83
|
+
| { kind: "start_and_stop_with_remainder"; remainder: string }
|
|
84
|
+
| { kind: "restart_only" }
|
|
85
|
+
| { kind: "restart_with_remainder"; remainder: string }
|
|
86
|
+
| { kind: "pause_only" }
|
|
87
|
+
| { kind: "resume_only" };
|
|
88
|
+
|
|
89
|
+
let mockIntentResult: RecordingIntentResult = { kind: "none" };
|
|
84
90
|
|
|
85
91
|
// Capture real function references BEFORE mock.module replaces the module.
|
|
86
92
|
// require() at this point returns the real module since mock.module has not
|
|
87
93
|
// been called yet for this specifier.
|
|
88
94
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
89
|
-
const _realRecordingIntentMod = require(
|
|
90
|
-
const _realResolveRecordingIntent =
|
|
95
|
+
const _realRecordingIntentMod = require("../daemon/recording-intent.js");
|
|
96
|
+
const _realResolveRecordingIntent =
|
|
97
|
+
_realRecordingIntentMod.resolveRecordingIntent;
|
|
91
98
|
const _realStripDynamicNames = _realRecordingIntentMod.stripDynamicNames;
|
|
92
99
|
|
|
93
100
|
// Flag: when true, the mock returns controlled test values; when false, it
|
|
@@ -95,7 +102,7 @@ const _realStripDynamicNames = _realRecordingIntentMod.stripDynamicNames;
|
|
|
95
102
|
// bleeds to other test files, those files get the real behavior.
|
|
96
103
|
(globalThis as any).__riHandlerUseMockIntent = false;
|
|
97
104
|
|
|
98
|
-
mock.module(
|
|
105
|
+
mock.module("../daemon/recording-intent.js", () => ({
|
|
99
106
|
resolveRecordingIntent: (...args: any[]) => {
|
|
100
107
|
if ((globalThis as any).__riHandlerUseMockIntent) return mockIntentResult;
|
|
101
108
|
return _realResolveRecordingIntent(...args);
|
|
@@ -129,20 +136,21 @@ let executorCalled = false;
|
|
|
129
136
|
let _realExecuteRecordingIntent: ((...args: any[]) => any) | null = null;
|
|
130
137
|
try {
|
|
131
138
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
132
|
-
const _mod = require(
|
|
139
|
+
const _mod = require("../daemon/recording-executor.js");
|
|
133
140
|
_realExecuteRecordingIntent = _mod.executeRecordingIntent;
|
|
134
141
|
} catch {
|
|
135
142
|
// Transitive dependency loading may fail when this file runs alone;
|
|
136
143
|
// the controlled mock will be used exclusively in that case.
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
mock.module(
|
|
146
|
+
mock.module("../daemon/recording-executor.js", () => ({
|
|
140
147
|
executeRecordingIntent: (...args: any[]) => {
|
|
141
148
|
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
142
149
|
executorCalled = true;
|
|
143
150
|
return mockExecuteResult;
|
|
144
151
|
}
|
|
145
|
-
if (_realExecuteRecordingIntent)
|
|
152
|
+
if (_realExecuteRecordingIntent)
|
|
153
|
+
return _realExecuteRecordingIntent(...args);
|
|
146
154
|
// Fallback if real function was not captured
|
|
147
155
|
return { handled: false };
|
|
148
156
|
},
|
|
@@ -172,7 +180,7 @@ let _realResetRecordingState: ((...args: any[]) => any) | null = null;
|
|
|
172
180
|
|
|
173
181
|
try {
|
|
174
182
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
175
|
-
const _mod = require(
|
|
183
|
+
const _mod = require("../daemon/handlers/recording.js");
|
|
176
184
|
_realHandleRecordingStart = _mod.handleRecordingStart;
|
|
177
185
|
_realHandleRecordingStop = _mod.handleRecordingStop;
|
|
178
186
|
_realHandleRecordingRestart = _mod.handleRecordingRestart;
|
|
@@ -185,39 +193,43 @@ try {
|
|
|
185
193
|
// Same as above — controlled mock will be used exclusively.
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
mock.module(
|
|
196
|
+
mock.module("../daemon/handlers/recording.js", () => ({
|
|
189
197
|
handleRecordingStart: (...args: any[]) => {
|
|
190
198
|
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
191
199
|
recordingStartCalled = true;
|
|
192
|
-
return
|
|
200
|
+
return "mock-recording-id";
|
|
193
201
|
}
|
|
194
202
|
return _realHandleRecordingStart?.(...args);
|
|
195
203
|
},
|
|
196
204
|
handleRecordingStop: (...args: any[]) => {
|
|
197
205
|
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
198
206
|
_recordingStopCalled = true;
|
|
199
|
-
return
|
|
207
|
+
return "mock-recording-id";
|
|
200
208
|
}
|
|
201
209
|
return _realHandleRecordingStop?.(...args);
|
|
202
210
|
},
|
|
203
211
|
handleRecordingRestart: (...args: any[]) => {
|
|
204
212
|
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
205
213
|
recordingRestartCalled = true;
|
|
206
|
-
return {
|
|
214
|
+
return {
|
|
215
|
+
initiated: true,
|
|
216
|
+
responseText: "Restarting screen recording.",
|
|
217
|
+
operationToken: "mock-token",
|
|
218
|
+
};
|
|
207
219
|
}
|
|
208
220
|
return _realHandleRecordingRestart?.(...args);
|
|
209
221
|
},
|
|
210
222
|
handleRecordingPause: (...args: any[]) => {
|
|
211
223
|
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
212
224
|
recordingPauseCalled = true;
|
|
213
|
-
return
|
|
225
|
+
return "mock-recording-id";
|
|
214
226
|
}
|
|
215
227
|
return _realHandleRecordingPause?.(...args);
|
|
216
228
|
},
|
|
217
229
|
handleRecordingResume: (...args: any[]) => {
|
|
218
230
|
if ((globalThis as any).__riHandlerUseMockIntent) {
|
|
219
231
|
recordingResumeCalled = true;
|
|
220
|
-
return
|
|
232
|
+
return "mock-recording-id";
|
|
221
233
|
}
|
|
222
234
|
return _realHandleRecordingResume?.(...args);
|
|
223
235
|
},
|
|
@@ -234,22 +246,28 @@ mock.module('../daemon/handlers/recording.js', () => ({
|
|
|
234
246
|
|
|
235
247
|
// ── Mock conversation store ────────────────────────────────────────────────
|
|
236
248
|
|
|
237
|
-
mock.module(
|
|
238
|
-
getConversationThreadType: () =>
|
|
249
|
+
mock.module("../memory/conversation-store.js", () => ({
|
|
250
|
+
getConversationThreadType: () => "default",
|
|
239
251
|
setConversationOriginChannelIfUnset: () => {},
|
|
240
252
|
updateConversationContextWindow: () => {},
|
|
241
253
|
deleteMessageById: () => {},
|
|
242
254
|
updateConversationUsage: () => {},
|
|
243
|
-
provenanceFromGuardianContext: () => ({
|
|
255
|
+
provenanceFromGuardianContext: () => ({
|
|
256
|
+
source: "user",
|
|
257
|
+
guardianContext: undefined,
|
|
258
|
+
}),
|
|
244
259
|
getConversationOriginInterface: () => null,
|
|
245
260
|
getConversationOriginChannel: () => null,
|
|
246
261
|
getMessages: () => [],
|
|
247
|
-
addMessage: () => ({ id:
|
|
262
|
+
addMessage: () => ({ id: "msg-mock", role: "assistant", content: "" }),
|
|
248
263
|
createConversation: (titleOrOpts?: string | { title?: string }) => {
|
|
249
|
-
const title =
|
|
250
|
-
|
|
264
|
+
const title =
|
|
265
|
+
typeof titleOrOpts === "string"
|
|
266
|
+
? titleOrOpts
|
|
267
|
+
: (titleOrOpts?.title ?? "Untitled");
|
|
268
|
+
return { id: "conv-mock", title };
|
|
251
269
|
},
|
|
252
|
-
getConversation: () => ({ id:
|
|
270
|
+
getConversation: () => ({ id: "conv-mock" }),
|
|
253
271
|
updateConversationTitle: noop,
|
|
254
272
|
clearAll: noop,
|
|
255
273
|
listConversations: () => [],
|
|
@@ -258,26 +276,26 @@ mock.module('../memory/conversation-store.js', () => ({
|
|
|
258
276
|
deleteConversation: noop,
|
|
259
277
|
}));
|
|
260
278
|
|
|
261
|
-
mock.module(
|
|
262
|
-
GENERATING_TITLE:
|
|
279
|
+
mock.module("../memory/conversation-title-service.js", () => ({
|
|
280
|
+
GENERATING_TITLE: "(generating\u2026)",
|
|
263
281
|
queueGenerateConversationTitle: noop,
|
|
264
|
-
UNTITLED_FALLBACK:
|
|
282
|
+
UNTITLED_FALLBACK: "Untitled",
|
|
265
283
|
}));
|
|
266
284
|
|
|
267
|
-
mock.module(
|
|
285
|
+
mock.module("../memory/attachments-store.js", () => ({
|
|
268
286
|
getAttachmentsForMessage: () => [],
|
|
269
|
-
uploadFileBackedAttachment: () => ({ id:
|
|
287
|
+
uploadFileBackedAttachment: () => ({ id: "att-mock" }),
|
|
270
288
|
linkAttachmentToMessage: noop,
|
|
271
289
|
setAttachmentThumbnail: noop,
|
|
272
290
|
}));
|
|
273
291
|
|
|
274
292
|
// ── Mock security ──────────────────────────────────────────────────────────
|
|
275
293
|
|
|
276
|
-
mock.module(
|
|
294
|
+
mock.module("../security/secret-ingress.js", () => ({
|
|
277
295
|
checkIngressForSecrets: () => ({ blocked: false }),
|
|
278
296
|
}));
|
|
279
297
|
|
|
280
|
-
mock.module(
|
|
298
|
+
mock.module("../security/secret-scanner.js", () => ({
|
|
281
299
|
redactSecrets: (text: string) => text,
|
|
282
300
|
compileCustomPatterns: () => [],
|
|
283
301
|
}));
|
|
@@ -286,44 +304,47 @@ mock.module('../security/secret-scanner.js', () => ({
|
|
|
286
304
|
|
|
287
305
|
let classifierCalled = false;
|
|
288
306
|
|
|
289
|
-
mock.module(
|
|
307
|
+
mock.module("../daemon/classifier.js", () => ({
|
|
290
308
|
classifyInteraction: async () => {
|
|
291
309
|
classifierCalled = true;
|
|
292
|
-
return
|
|
310
|
+
return "text_qa";
|
|
293
311
|
},
|
|
294
312
|
}));
|
|
295
313
|
|
|
296
314
|
// ── Mock slash commands ────────────────────────────────────────────────────
|
|
297
315
|
|
|
298
|
-
mock.module(
|
|
299
|
-
parseSlashCandidate: () => ({ kind:
|
|
316
|
+
mock.module("../skills/slash-commands.js", () => ({
|
|
317
|
+
parseSlashCandidate: () => ({ kind: "none" }),
|
|
300
318
|
}));
|
|
301
319
|
|
|
302
320
|
// ── Mock computer-use handler ──────────────────────────────────────────────
|
|
303
321
|
|
|
304
|
-
mock.module(
|
|
322
|
+
mock.module("../daemon/handlers/computer-use.js", () => ({
|
|
305
323
|
handleCuSessionCreate: noop,
|
|
306
324
|
}));
|
|
307
325
|
|
|
308
326
|
// ── Mock provider ──────────────────────────────────────────────────────────
|
|
309
327
|
|
|
310
|
-
mock.module(
|
|
328
|
+
mock.module("../providers/provider-send-message.js", () => ({
|
|
311
329
|
getConfiguredProvider: () => null,
|
|
312
|
-
extractText: (_response: unknown) =>
|
|
313
|
-
createTimeout: (_ms: number) => ({
|
|
314
|
-
|
|
330
|
+
extractText: (_response: unknown) => "",
|
|
331
|
+
createTimeout: (_ms: number) => ({
|
|
332
|
+
signal: new AbortController().signal,
|
|
333
|
+
cleanup: () => {},
|
|
334
|
+
}),
|
|
335
|
+
userMessage: (text: string) => ({ role: "user", content: text }),
|
|
315
336
|
}));
|
|
316
337
|
|
|
317
338
|
// ── Mock external conversation store ───────────────────────────────────────
|
|
318
339
|
|
|
319
|
-
mock.module(
|
|
340
|
+
mock.module("../memory/external-conversation-store.js", () => ({
|
|
320
341
|
getBindingsForConversations: () => new Map(),
|
|
321
342
|
upsertBinding: () => {},
|
|
322
343
|
}));
|
|
323
344
|
|
|
324
345
|
// ── Mock subagent manager ──────────────────────────────────────────────────
|
|
325
346
|
|
|
326
|
-
mock.module(
|
|
347
|
+
mock.module("../subagent/index.js", () => ({
|
|
327
348
|
getSubagentManager: () => ({
|
|
328
349
|
abortAllForParent: noop,
|
|
329
350
|
}),
|
|
@@ -331,43 +352,47 @@ mock.module('../subagent/index.js', () => ({
|
|
|
331
352
|
|
|
332
353
|
// ── Mock IPC protocol helpers ──────────────────────────────────────────────
|
|
333
354
|
|
|
334
|
-
mock.module(
|
|
335
|
-
normalizeThreadType: (t: string) => t ??
|
|
355
|
+
mock.module("../daemon/ipc-protocol.js", () => ({
|
|
356
|
+
normalizeThreadType: (t: string) => t ?? "primary",
|
|
336
357
|
}));
|
|
337
358
|
|
|
338
359
|
// ── Mock session error helpers ─────────────────────────────────────────────
|
|
339
360
|
|
|
340
|
-
mock.module(
|
|
341
|
-
classifySessionError: () => ({
|
|
342
|
-
|
|
361
|
+
mock.module("../daemon/session-error.js", () => ({
|
|
362
|
+
classifySessionError: () => ({
|
|
363
|
+
code: "UNKNOWN",
|
|
364
|
+
userMessage: "error",
|
|
365
|
+
retryable: false,
|
|
366
|
+
}),
|
|
367
|
+
buildSessionErrorMessage: () => ({ type: "error", message: "error" }),
|
|
343
368
|
}));
|
|
344
369
|
|
|
345
370
|
// ── Mock video thumbnail ───────────────────────────────────────────────────
|
|
346
371
|
|
|
347
|
-
mock.module(
|
|
372
|
+
mock.module("../daemon/video-thumbnail.js", () => ({
|
|
348
373
|
generateVideoThumbnail: async () => null,
|
|
349
374
|
}));
|
|
350
375
|
|
|
351
376
|
// ── Mock IPC blob store ────────────────────────────────────────────────────
|
|
352
377
|
|
|
353
|
-
mock.module(
|
|
378
|
+
mock.module("../daemon/ipc-blob-store.js", () => ({
|
|
354
379
|
isValidBlobId: () => false,
|
|
355
|
-
resolveBlobPath: () =>
|
|
380
|
+
resolveBlobPath: () => "",
|
|
356
381
|
deleteBlob: noop,
|
|
357
382
|
}));
|
|
358
383
|
|
|
359
384
|
// ── Mock channels/types ────────────────────────────────────────────────────
|
|
360
385
|
|
|
361
|
-
mock.module(
|
|
362
|
-
parseChannelId: () =>
|
|
363
|
-
parseInterfaceId: () =>
|
|
386
|
+
mock.module("../channels/types.js", () => ({
|
|
387
|
+
parseChannelId: () => "vellum",
|
|
388
|
+
parseInterfaceId: () => "vellum",
|
|
364
389
|
isChannelId: () => true,
|
|
365
390
|
}));
|
|
366
391
|
|
|
367
392
|
// ─── Imports (after mocks) ──────────────────────────────────────────────────
|
|
368
393
|
|
|
369
|
-
import type { HandlerContext } from
|
|
370
|
-
import { DebouncerMap } from
|
|
394
|
+
import type { HandlerContext } from "../daemon/handlers/shared.js";
|
|
395
|
+
import { DebouncerMap } from "../util/debounce.js";
|
|
371
396
|
|
|
372
397
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
373
398
|
|
|
@@ -418,7 +443,9 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
418
443
|
suppressConfigReload: false,
|
|
419
444
|
setSuppressConfigReload: noop,
|
|
420
445
|
updateConfigFingerprint: noop,
|
|
421
|
-
send: (_socket, msg) => {
|
|
446
|
+
send: (_socket, msg) => {
|
|
447
|
+
sent.push(msg as { type: string; [k: string]: unknown });
|
|
448
|
+
},
|
|
422
449
|
broadcast: noop,
|
|
423
450
|
clearAllSessions: () => 0,
|
|
424
451
|
getOrCreateSession: async (conversationId: string) => {
|
|
@@ -435,7 +462,7 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
435
462
|
function resetMockState(): void {
|
|
436
463
|
// Enable mock mode for this file's tests
|
|
437
464
|
(globalThis as any).__riHandlerUseMockIntent = true;
|
|
438
|
-
mockIntentResult = { kind:
|
|
465
|
+
mockIntentResult = { kind: "none" };
|
|
439
466
|
mockExecuteResult = { handled: false };
|
|
440
467
|
mockAssistantName = null;
|
|
441
468
|
recordingStartCalled = false;
|
|
@@ -455,17 +482,21 @@ afterAll(() => {
|
|
|
455
482
|
|
|
456
483
|
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
457
484
|
|
|
458
|
-
describe(
|
|
485
|
+
describe("recording intent handler integration — handleTaskSubmit", () => {
|
|
459
486
|
beforeEach(resetMockState);
|
|
460
487
|
|
|
461
|
-
test(
|
|
462
|
-
mockIntentResult = { kind:
|
|
463
|
-
mockExecuteResult = {
|
|
488
|
+
test("start_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early", async () => {
|
|
489
|
+
mockIntentResult = { kind: "start_only" };
|
|
490
|
+
mockExecuteResult = {
|
|
491
|
+
handled: true,
|
|
492
|
+
responseText: "Starting screen recording.",
|
|
493
|
+
recordingStarted: true,
|
|
494
|
+
};
|
|
464
495
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
465
496
|
|
|
466
|
-
const { handleTaskSubmit } = await import(
|
|
497
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
467
498
|
await handleTaskSubmit(
|
|
468
|
-
{ type:
|
|
499
|
+
{ type: "task_submit", task: "record my screen", source: "voice" } as any,
|
|
469
500
|
fakeSocket,
|
|
470
501
|
ctx,
|
|
471
502
|
);
|
|
@@ -474,22 +505,25 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
474
505
|
expect(classifierCalled).toBe(false);
|
|
475
506
|
|
|
476
507
|
const types = sent.map((m) => m.type);
|
|
477
|
-
expect(types).toContain(
|
|
478
|
-
expect(types).toContain(
|
|
479
|
-
expect(types).toContain(
|
|
508
|
+
expect(types).toContain("task_routed");
|
|
509
|
+
expect(types).toContain("assistant_text_delta");
|
|
510
|
+
expect(types).toContain("message_complete");
|
|
480
511
|
|
|
481
|
-
const textDelta = sent.find((m) => m.type ===
|
|
482
|
-
expect(textDelta?.text).toBe(
|
|
512
|
+
const textDelta = sent.find((m) => m.type === "assistant_text_delta");
|
|
513
|
+
expect(textDelta?.text).toBe("Starting screen recording.");
|
|
483
514
|
});
|
|
484
515
|
|
|
485
|
-
test(
|
|
486
|
-
mockIntentResult = { kind:
|
|
487
|
-
mockExecuteResult = {
|
|
516
|
+
test("stop_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early", async () => {
|
|
517
|
+
mockIntentResult = { kind: "stop_only" };
|
|
518
|
+
mockExecuteResult = {
|
|
519
|
+
handled: true,
|
|
520
|
+
responseText: "Stopping the recording.",
|
|
521
|
+
};
|
|
488
522
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
489
523
|
|
|
490
|
-
const { handleTaskSubmit } = await import(
|
|
524
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
491
525
|
await handleTaskSubmit(
|
|
492
|
-
{ type:
|
|
526
|
+
{ type: "task_submit", task: "stop recording", source: "voice" } as any,
|
|
493
527
|
fakeSocket,
|
|
494
528
|
ctx,
|
|
495
529
|
);
|
|
@@ -498,22 +532,33 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
498
532
|
expect(classifierCalled).toBe(false);
|
|
499
533
|
|
|
500
534
|
const types = sent.map((m) => m.type);
|
|
501
|
-
expect(types).toContain(
|
|
502
|
-
expect(types).toContain(
|
|
503
|
-
expect(types).toContain(
|
|
535
|
+
expect(types).toContain("task_routed");
|
|
536
|
+
expect(types).toContain("assistant_text_delta");
|
|
537
|
+
expect(types).toContain("message_complete");
|
|
504
538
|
|
|
505
|
-
const textDelta = sent.find((m) => m.type ===
|
|
506
|
-
expect(textDelta?.text).toBe(
|
|
539
|
+
const textDelta = sent.find((m) => m.type === "assistant_text_delta");
|
|
540
|
+
expect(textDelta?.text).toBe("Stopping the recording.");
|
|
507
541
|
});
|
|
508
542
|
|
|
509
|
-
test(
|
|
510
|
-
mockIntentResult = {
|
|
511
|
-
|
|
543
|
+
test("start_with_remainder → defers recording, falls through to classifier with remaining text", async () => {
|
|
544
|
+
mockIntentResult = {
|
|
545
|
+
kind: "start_with_remainder",
|
|
546
|
+
remainder: "open Safari",
|
|
547
|
+
};
|
|
548
|
+
mockExecuteResult = {
|
|
549
|
+
handled: false,
|
|
550
|
+
remainderText: "open Safari",
|
|
551
|
+
pendingStart: true,
|
|
552
|
+
};
|
|
512
553
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
513
554
|
|
|
514
|
-
const { handleTaskSubmit } = await import(
|
|
555
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
515
556
|
await handleTaskSubmit(
|
|
516
|
-
{
|
|
557
|
+
{
|
|
558
|
+
type: "task_submit",
|
|
559
|
+
task: "open Safari and record my screen",
|
|
560
|
+
source: "voice",
|
|
561
|
+
} as any,
|
|
517
562
|
fakeSocket,
|
|
518
563
|
ctx,
|
|
519
564
|
);
|
|
@@ -522,19 +567,22 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
522
567
|
|
|
523
568
|
// Should NOT have recording-only messages before the classifier output
|
|
524
569
|
const recordingSpecific = sent.filter(
|
|
525
|
-
(m) =>
|
|
526
|
-
|
|
570
|
+
(m) =>
|
|
571
|
+
m.type === "assistant_text_delta" &&
|
|
572
|
+
typeof m.text === "string" &&
|
|
573
|
+
(m.text.includes("Starting screen recording") ||
|
|
574
|
+
m.text.includes("Stopping the recording")),
|
|
527
575
|
);
|
|
528
576
|
expect(recordingSpecific).toHaveLength(0);
|
|
529
577
|
});
|
|
530
578
|
|
|
531
|
-
test(
|
|
532
|
-
mockIntentResult = { kind:
|
|
579
|
+
test("none → does NOT call executeRecordingIntent, falls through to classifier", async () => {
|
|
580
|
+
mockIntentResult = { kind: "none" };
|
|
533
581
|
const { ctx, sent: _sent, fakeSocket } = createCtx();
|
|
534
582
|
|
|
535
|
-
const { handleTaskSubmit } = await import(
|
|
583
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
536
584
|
await handleTaskSubmit(
|
|
537
|
-
{ type:
|
|
585
|
+
{ type: "task_submit", task: "hello world", source: "voice" } as any,
|
|
538
586
|
fakeSocket,
|
|
539
587
|
ctx,
|
|
540
588
|
);
|
|
@@ -543,14 +591,21 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
543
591
|
expect(classifierCalled).toBe(true);
|
|
544
592
|
});
|
|
545
593
|
|
|
546
|
-
test(
|
|
547
|
-
mockIntentResult = { kind:
|
|
548
|
-
mockExecuteResult = {
|
|
594
|
+
test("restart_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early", async () => {
|
|
595
|
+
mockIntentResult = { kind: "restart_only" };
|
|
596
|
+
mockExecuteResult = {
|
|
597
|
+
handled: true,
|
|
598
|
+
responseText: "Restarting screen recording.",
|
|
599
|
+
};
|
|
549
600
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
550
601
|
|
|
551
|
-
const { handleTaskSubmit } = await import(
|
|
602
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
552
603
|
await handleTaskSubmit(
|
|
553
|
-
{
|
|
604
|
+
{
|
|
605
|
+
type: "task_submit",
|
|
606
|
+
task: "restart the recording",
|
|
607
|
+
source: "voice",
|
|
608
|
+
} as any,
|
|
554
609
|
fakeSocket,
|
|
555
610
|
ctx,
|
|
556
611
|
);
|
|
@@ -559,22 +614,29 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
559
614
|
expect(classifierCalled).toBe(false);
|
|
560
615
|
|
|
561
616
|
const types = sent.map((m) => m.type);
|
|
562
|
-
expect(types).toContain(
|
|
563
|
-
expect(types).toContain(
|
|
564
|
-
expect(types).toContain(
|
|
617
|
+
expect(types).toContain("task_routed");
|
|
618
|
+
expect(types).toContain("assistant_text_delta");
|
|
619
|
+
expect(types).toContain("message_complete");
|
|
565
620
|
|
|
566
|
-
const textDelta = sent.find((m) => m.type ===
|
|
567
|
-
expect(textDelta?.text).toBe(
|
|
621
|
+
const textDelta = sent.find((m) => m.type === "assistant_text_delta");
|
|
622
|
+
expect(textDelta?.text).toBe("Restarting screen recording.");
|
|
568
623
|
});
|
|
569
624
|
|
|
570
|
-
test(
|
|
571
|
-
mockIntentResult = { kind:
|
|
572
|
-
mockExecuteResult = {
|
|
625
|
+
test("pause_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early", async () => {
|
|
626
|
+
mockIntentResult = { kind: "pause_only" };
|
|
627
|
+
mockExecuteResult = {
|
|
628
|
+
handled: true,
|
|
629
|
+
responseText: "Pausing the recording.",
|
|
630
|
+
};
|
|
573
631
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
574
632
|
|
|
575
|
-
const { handleTaskSubmit } = await import(
|
|
633
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
576
634
|
await handleTaskSubmit(
|
|
577
|
-
{
|
|
635
|
+
{
|
|
636
|
+
type: "task_submit",
|
|
637
|
+
task: "pause the recording",
|
|
638
|
+
source: "voice",
|
|
639
|
+
} as any,
|
|
578
640
|
fakeSocket,
|
|
579
641
|
ctx,
|
|
580
642
|
);
|
|
@@ -583,22 +645,29 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
583
645
|
expect(classifierCalled).toBe(false);
|
|
584
646
|
|
|
585
647
|
const types = sent.map((m) => m.type);
|
|
586
|
-
expect(types).toContain(
|
|
587
|
-
expect(types).toContain(
|
|
588
|
-
expect(types).toContain(
|
|
648
|
+
expect(types).toContain("task_routed");
|
|
649
|
+
expect(types).toContain("assistant_text_delta");
|
|
650
|
+
expect(types).toContain("message_complete");
|
|
589
651
|
|
|
590
|
-
const textDelta = sent.find((m) => m.type ===
|
|
591
|
-
expect(textDelta?.text).toBe(
|
|
652
|
+
const textDelta = sent.find((m) => m.type === "assistant_text_delta");
|
|
653
|
+
expect(textDelta?.text).toBe("Pausing the recording.");
|
|
592
654
|
});
|
|
593
655
|
|
|
594
|
-
test(
|
|
595
|
-
mockIntentResult = { kind:
|
|
596
|
-
mockExecuteResult = {
|
|
656
|
+
test("resume_only → executeRecordingIntent called, sends task_routed + text_delta + message_complete, returns early", async () => {
|
|
657
|
+
mockIntentResult = { kind: "resume_only" };
|
|
658
|
+
mockExecuteResult = {
|
|
659
|
+
handled: true,
|
|
660
|
+
responseText: "Resuming the recording.",
|
|
661
|
+
};
|
|
597
662
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
598
663
|
|
|
599
|
-
const { handleTaskSubmit } = await import(
|
|
664
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
600
665
|
await handleTaskSubmit(
|
|
601
|
-
{
|
|
666
|
+
{
|
|
667
|
+
type: "task_submit",
|
|
668
|
+
task: "resume the recording",
|
|
669
|
+
source: "voice",
|
|
670
|
+
} as any,
|
|
602
671
|
fakeSocket,
|
|
603
672
|
ctx,
|
|
604
673
|
);
|
|
@@ -607,22 +676,33 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
607
676
|
expect(classifierCalled).toBe(false);
|
|
608
677
|
|
|
609
678
|
const types = sent.map((m) => m.type);
|
|
610
|
-
expect(types).toContain(
|
|
611
|
-
expect(types).toContain(
|
|
612
|
-
expect(types).toContain(
|
|
679
|
+
expect(types).toContain("task_routed");
|
|
680
|
+
expect(types).toContain("assistant_text_delta");
|
|
681
|
+
expect(types).toContain("message_complete");
|
|
613
682
|
|
|
614
|
-
const textDelta = sent.find((m) => m.type ===
|
|
615
|
-
expect(textDelta?.text).toBe(
|
|
683
|
+
const textDelta = sent.find((m) => m.type === "assistant_text_delta");
|
|
684
|
+
expect(textDelta?.text).toBe("Resuming the recording.");
|
|
616
685
|
});
|
|
617
686
|
|
|
618
|
-
test(
|
|
619
|
-
mockIntentResult = {
|
|
620
|
-
|
|
687
|
+
test("restart_with_remainder → defers restart, falls through to classifier with remaining text", async () => {
|
|
688
|
+
mockIntentResult = {
|
|
689
|
+
kind: "restart_with_remainder",
|
|
690
|
+
remainder: "open Safari",
|
|
691
|
+
};
|
|
692
|
+
mockExecuteResult = {
|
|
693
|
+
handled: false,
|
|
694
|
+
remainderText: "open Safari",
|
|
695
|
+
pendingRestart: true,
|
|
696
|
+
};
|
|
621
697
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
622
698
|
|
|
623
|
-
const { handleTaskSubmit } = await import(
|
|
699
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
624
700
|
await handleTaskSubmit(
|
|
625
|
-
{
|
|
701
|
+
{
|
|
702
|
+
type: "task_submit",
|
|
703
|
+
task: "restart the recording and open Safari",
|
|
704
|
+
source: "voice",
|
|
705
|
+
} as any,
|
|
626
706
|
fakeSocket,
|
|
627
707
|
ctx,
|
|
628
708
|
);
|
|
@@ -631,24 +711,26 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
631
711
|
|
|
632
712
|
// Should NOT have restart-specific messages before classifier output
|
|
633
713
|
const recordingSpecific = sent.filter(
|
|
634
|
-
(m) =>
|
|
635
|
-
m.
|
|
714
|
+
(m) =>
|
|
715
|
+
m.type === "assistant_text_delta" &&
|
|
716
|
+
typeof m.text === "string" &&
|
|
717
|
+
m.text.includes("Restarting screen recording"),
|
|
636
718
|
);
|
|
637
719
|
expect(recordingSpecific).toHaveLength(0);
|
|
638
720
|
});
|
|
639
721
|
|
|
640
|
-
test(
|
|
722
|
+
test("commandIntent restart → routes directly via handleRecordingRestart, returns early", async () => {
|
|
641
723
|
// commandIntent bypasses text-based intent resolution entirely
|
|
642
|
-
mockIntentResult = { kind:
|
|
724
|
+
mockIntentResult = { kind: "none" }; // should not matter
|
|
643
725
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
644
726
|
|
|
645
|
-
const { handleTaskSubmit } = await import(
|
|
727
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
646
728
|
await handleTaskSubmit(
|
|
647
729
|
{
|
|
648
|
-
type:
|
|
649
|
-
task:
|
|
650
|
-
source:
|
|
651
|
-
commandIntent: { domain:
|
|
730
|
+
type: "task_submit",
|
|
731
|
+
task: "restart recording",
|
|
732
|
+
source: "voice",
|
|
733
|
+
commandIntent: { domain: "screen_recording", action: "restart" },
|
|
652
734
|
} as any,
|
|
653
735
|
fakeSocket,
|
|
654
736
|
ctx,
|
|
@@ -658,22 +740,22 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
658
740
|
expect(classifierCalled).toBe(false);
|
|
659
741
|
|
|
660
742
|
const types = sent.map((m) => m.type);
|
|
661
|
-
expect(types).toContain(
|
|
662
|
-
expect(types).toContain(
|
|
663
|
-
expect(types).toContain(
|
|
743
|
+
expect(types).toContain("task_routed");
|
|
744
|
+
expect(types).toContain("assistant_text_delta");
|
|
745
|
+
expect(types).toContain("message_complete");
|
|
664
746
|
});
|
|
665
747
|
|
|
666
|
-
test(
|
|
667
|
-
mockIntentResult = { kind:
|
|
748
|
+
test("commandIntent pause → routes directly via handleRecordingPause, returns early", async () => {
|
|
749
|
+
mockIntentResult = { kind: "none" };
|
|
668
750
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
669
751
|
|
|
670
|
-
const { handleTaskSubmit } = await import(
|
|
752
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
671
753
|
await handleTaskSubmit(
|
|
672
754
|
{
|
|
673
|
-
type:
|
|
674
|
-
task:
|
|
675
|
-
source:
|
|
676
|
-
commandIntent: { domain:
|
|
755
|
+
type: "task_submit",
|
|
756
|
+
task: "pause recording",
|
|
757
|
+
source: "voice",
|
|
758
|
+
commandIntent: { domain: "screen_recording", action: "pause" },
|
|
677
759
|
} as any,
|
|
678
760
|
fakeSocket,
|
|
679
761
|
ctx,
|
|
@@ -683,22 +765,22 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
683
765
|
expect(classifierCalled).toBe(false);
|
|
684
766
|
|
|
685
767
|
const types = sent.map((m) => m.type);
|
|
686
|
-
expect(types).toContain(
|
|
687
|
-
expect(types).toContain(
|
|
688
|
-
expect(types).toContain(
|
|
768
|
+
expect(types).toContain("task_routed");
|
|
769
|
+
expect(types).toContain("assistant_text_delta");
|
|
770
|
+
expect(types).toContain("message_complete");
|
|
689
771
|
});
|
|
690
772
|
|
|
691
|
-
test(
|
|
692
|
-
mockIntentResult = { kind:
|
|
773
|
+
test("commandIntent resume → routes directly via handleRecordingResume, returns early", async () => {
|
|
774
|
+
mockIntentResult = { kind: "none" };
|
|
693
775
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
694
776
|
|
|
695
|
-
const { handleTaskSubmit } = await import(
|
|
777
|
+
const { handleTaskSubmit } = await import("../daemon/handlers/misc.js");
|
|
696
778
|
await handleTaskSubmit(
|
|
697
779
|
{
|
|
698
|
-
type:
|
|
699
|
-
task:
|
|
700
|
-
source:
|
|
701
|
-
commandIntent: { domain:
|
|
780
|
+
type: "task_submit",
|
|
781
|
+
task: "resume recording",
|
|
782
|
+
source: "voice",
|
|
783
|
+
commandIntent: { domain: "screen_recording", action: "resume" },
|
|
702
784
|
} as any,
|
|
703
785
|
fakeSocket,
|
|
704
786
|
ctx,
|
|
@@ -708,27 +790,32 @@ describe('recording intent handler integration — handleTaskSubmit', () => {
|
|
|
708
790
|
expect(classifierCalled).toBe(false);
|
|
709
791
|
|
|
710
792
|
const types = sent.map((m) => m.type);
|
|
711
|
-
expect(types).toContain(
|
|
712
|
-
expect(types).toContain(
|
|
713
|
-
expect(types).toContain(
|
|
793
|
+
expect(types).toContain("task_routed");
|
|
794
|
+
expect(types).toContain("assistant_text_delta");
|
|
795
|
+
expect(types).toContain("message_complete");
|
|
714
796
|
});
|
|
715
797
|
});
|
|
716
798
|
|
|
717
|
-
describe(
|
|
799
|
+
describe("recording intent handler integration — handleUserMessage", () => {
|
|
718
800
|
beforeEach(resetMockState);
|
|
719
801
|
|
|
720
|
-
test(
|
|
721
|
-
mockIntentResult = { kind:
|
|
722
|
-
mockExecuteResult = {
|
|
802
|
+
test("start_only → executeRecordingIntent called, sends text_delta + message_complete, returns early", async () => {
|
|
803
|
+
mockIntentResult = { kind: "start_only" };
|
|
804
|
+
mockExecuteResult = {
|
|
805
|
+
handled: true,
|
|
806
|
+
responseText: "Starting screen recording.",
|
|
807
|
+
recordingStarted: true,
|
|
808
|
+
};
|
|
723
809
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
724
810
|
|
|
725
|
-
const { handleUserMessage } =
|
|
811
|
+
const { handleUserMessage } =
|
|
812
|
+
await import("../daemon/handlers/sessions.js");
|
|
726
813
|
await handleUserMessage(
|
|
727
814
|
{
|
|
728
|
-
type:
|
|
729
|
-
sessionId:
|
|
730
|
-
content:
|
|
731
|
-
interface:
|
|
815
|
+
type: "user_message",
|
|
816
|
+
sessionId: "test-session",
|
|
817
|
+
content: "record my screen",
|
|
818
|
+
interface: "vellum",
|
|
732
819
|
} as any,
|
|
733
820
|
fakeSocket,
|
|
734
821
|
ctx,
|
|
@@ -737,26 +824,30 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
737
824
|
expect(executorCalled).toBe(true);
|
|
738
825
|
|
|
739
826
|
const types = sent.map((m) => m.type);
|
|
740
|
-
expect(types).toContain(
|
|
741
|
-
expect(types).toContain(
|
|
827
|
+
expect(types).toContain("assistant_text_delta");
|
|
828
|
+
expect(types).toContain("message_complete");
|
|
742
829
|
|
|
743
830
|
// message_complete should be the last message sent (recording returned early)
|
|
744
831
|
const lastMsg = sent[sent.length - 1];
|
|
745
|
-
expect(lastMsg.type).toBe(
|
|
832
|
+
expect(lastMsg.type).toBe("message_complete");
|
|
746
833
|
});
|
|
747
834
|
|
|
748
|
-
test(
|
|
749
|
-
mockIntentResult = { kind:
|
|
750
|
-
mockExecuteResult = {
|
|
835
|
+
test("stop_only → executeRecordingIntent called, sends text_delta + message_complete, returns early", async () => {
|
|
836
|
+
mockIntentResult = { kind: "stop_only" };
|
|
837
|
+
mockExecuteResult = {
|
|
838
|
+
handled: true,
|
|
839
|
+
responseText: "Stopping the recording.",
|
|
840
|
+
};
|
|
751
841
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
752
842
|
|
|
753
|
-
const { handleUserMessage } =
|
|
843
|
+
const { handleUserMessage } =
|
|
844
|
+
await import("../daemon/handlers/sessions.js");
|
|
754
845
|
await handleUserMessage(
|
|
755
846
|
{
|
|
756
|
-
type:
|
|
757
|
-
sessionId:
|
|
758
|
-
content:
|
|
759
|
-
interface:
|
|
847
|
+
type: "user_message",
|
|
848
|
+
sessionId: "test-session",
|
|
849
|
+
content: "stop recording",
|
|
850
|
+
interface: "vellum",
|
|
760
851
|
} as any,
|
|
761
852
|
fakeSocket,
|
|
762
853
|
ctx,
|
|
@@ -765,25 +856,33 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
765
856
|
expect(executorCalled).toBe(true);
|
|
766
857
|
|
|
767
858
|
const types = sent.map((m) => m.type);
|
|
768
|
-
expect(types).toContain(
|
|
769
|
-
expect(types).toContain(
|
|
859
|
+
expect(types).toContain("assistant_text_delta");
|
|
860
|
+
expect(types).toContain("message_complete");
|
|
770
861
|
|
|
771
862
|
const lastMsg = sent[sent.length - 1];
|
|
772
|
-
expect(lastMsg.type).toBe(
|
|
863
|
+
expect(lastMsg.type).toBe("message_complete");
|
|
773
864
|
});
|
|
774
865
|
|
|
775
|
-
test(
|
|
776
|
-
mockIntentResult = {
|
|
777
|
-
|
|
866
|
+
test("start_with_remainder → does NOT return early, proceeds to normal message processing", async () => {
|
|
867
|
+
mockIntentResult = {
|
|
868
|
+
kind: "start_with_remainder",
|
|
869
|
+
remainder: "open Safari",
|
|
870
|
+
};
|
|
871
|
+
mockExecuteResult = {
|
|
872
|
+
handled: false,
|
|
873
|
+
remainderText: "open Safari",
|
|
874
|
+
pendingStart: true,
|
|
875
|
+
};
|
|
778
876
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
779
877
|
|
|
780
|
-
const { handleUserMessage } =
|
|
878
|
+
const { handleUserMessage } =
|
|
879
|
+
await import("../daemon/handlers/sessions.js");
|
|
781
880
|
await handleUserMessage(
|
|
782
881
|
{
|
|
783
|
-
type:
|
|
784
|
-
sessionId:
|
|
785
|
-
content:
|
|
786
|
-
interface:
|
|
882
|
+
type: "user_message",
|
|
883
|
+
sessionId: "test-session",
|
|
884
|
+
content: "open Safari and record my screen",
|
|
885
|
+
interface: "vellum",
|
|
787
886
|
} as any,
|
|
788
887
|
fakeSocket,
|
|
789
888
|
ctx,
|
|
@@ -794,23 +893,27 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
794
893
|
|
|
795
894
|
// Should NOT have recording-specific intercept messages
|
|
796
895
|
const recordingSpecific = sent.filter(
|
|
797
|
-
(m) =>
|
|
798
|
-
|
|
896
|
+
(m) =>
|
|
897
|
+
m.type === "assistant_text_delta" &&
|
|
898
|
+
typeof m.text === "string" &&
|
|
899
|
+
(m.text.includes("Starting screen recording") ||
|
|
900
|
+
m.text.includes("Stopping the recording")),
|
|
799
901
|
);
|
|
800
902
|
expect(recordingSpecific).toHaveLength(0);
|
|
801
903
|
});
|
|
802
904
|
|
|
803
|
-
test(
|
|
804
|
-
mockIntentResult = { kind:
|
|
905
|
+
test("none → does NOT intercept, proceeds to normal message processing", async () => {
|
|
906
|
+
mockIntentResult = { kind: "none" };
|
|
805
907
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
806
908
|
|
|
807
|
-
const { handleUserMessage } =
|
|
909
|
+
const { handleUserMessage } =
|
|
910
|
+
await import("../daemon/handlers/sessions.js");
|
|
808
911
|
await handleUserMessage(
|
|
809
912
|
{
|
|
810
|
-
type:
|
|
811
|
-
sessionId:
|
|
812
|
-
content:
|
|
813
|
-
interface:
|
|
913
|
+
type: "user_message",
|
|
914
|
+
sessionId: "test-session",
|
|
915
|
+
content: "hello world",
|
|
916
|
+
interface: "vellum",
|
|
814
917
|
} as any,
|
|
815
918
|
fakeSocket,
|
|
816
919
|
ctx,
|
|
@@ -820,24 +923,31 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
820
923
|
|
|
821
924
|
// Should NOT have recording-specific messages
|
|
822
925
|
const recordingSpecific = sent.filter(
|
|
823
|
-
(m) =>
|
|
824
|
-
|
|
926
|
+
(m) =>
|
|
927
|
+
m.type === "assistant_text_delta" &&
|
|
928
|
+
typeof m.text === "string" &&
|
|
929
|
+
(m.text.includes("Starting screen recording") ||
|
|
930
|
+
m.text.includes("Stopping the recording")),
|
|
825
931
|
);
|
|
826
932
|
expect(recordingSpecific).toHaveLength(0);
|
|
827
933
|
});
|
|
828
934
|
|
|
829
|
-
test(
|
|
830
|
-
mockIntentResult = { kind:
|
|
831
|
-
mockExecuteResult = {
|
|
935
|
+
test("restart_only → executeRecordingIntent called, sends text_delta + message_complete, returns early", async () => {
|
|
936
|
+
mockIntentResult = { kind: "restart_only" };
|
|
937
|
+
mockExecuteResult = {
|
|
938
|
+
handled: true,
|
|
939
|
+
responseText: "Restarting screen recording.",
|
|
940
|
+
};
|
|
832
941
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
833
942
|
|
|
834
|
-
const { handleUserMessage } =
|
|
943
|
+
const { handleUserMessage } =
|
|
944
|
+
await import("../daemon/handlers/sessions.js");
|
|
835
945
|
await handleUserMessage(
|
|
836
946
|
{
|
|
837
|
-
type:
|
|
838
|
-
sessionId:
|
|
839
|
-
content:
|
|
840
|
-
interface:
|
|
947
|
+
type: "user_message",
|
|
948
|
+
sessionId: "test-session",
|
|
949
|
+
content: "restart the recording",
|
|
950
|
+
interface: "vellum",
|
|
841
951
|
} as any,
|
|
842
952
|
fakeSocket,
|
|
843
953
|
ctx,
|
|
@@ -846,25 +956,29 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
846
956
|
expect(executorCalled).toBe(true);
|
|
847
957
|
|
|
848
958
|
const types = sent.map((m) => m.type);
|
|
849
|
-
expect(types).toContain(
|
|
850
|
-
expect(types).toContain(
|
|
959
|
+
expect(types).toContain("assistant_text_delta");
|
|
960
|
+
expect(types).toContain("message_complete");
|
|
851
961
|
|
|
852
962
|
const lastMsg = sent[sent.length - 1];
|
|
853
|
-
expect(lastMsg.type).toBe(
|
|
963
|
+
expect(lastMsg.type).toBe("message_complete");
|
|
854
964
|
});
|
|
855
965
|
|
|
856
|
-
test(
|
|
857
|
-
mockIntentResult = { kind:
|
|
858
|
-
mockExecuteResult = {
|
|
966
|
+
test("pause_only → executeRecordingIntent called, sends text_delta + message_complete, returns early", async () => {
|
|
967
|
+
mockIntentResult = { kind: "pause_only" };
|
|
968
|
+
mockExecuteResult = {
|
|
969
|
+
handled: true,
|
|
970
|
+
responseText: "Pausing the recording.",
|
|
971
|
+
};
|
|
859
972
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
860
973
|
|
|
861
|
-
const { handleUserMessage } =
|
|
974
|
+
const { handleUserMessage } =
|
|
975
|
+
await import("../daemon/handlers/sessions.js");
|
|
862
976
|
await handleUserMessage(
|
|
863
977
|
{
|
|
864
|
-
type:
|
|
865
|
-
sessionId:
|
|
866
|
-
content:
|
|
867
|
-
interface:
|
|
978
|
+
type: "user_message",
|
|
979
|
+
sessionId: "test-session",
|
|
980
|
+
content: "pause the recording",
|
|
981
|
+
interface: "vellum",
|
|
868
982
|
} as any,
|
|
869
983
|
fakeSocket,
|
|
870
984
|
ctx,
|
|
@@ -873,25 +987,29 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
873
987
|
expect(executorCalled).toBe(true);
|
|
874
988
|
|
|
875
989
|
const types = sent.map((m) => m.type);
|
|
876
|
-
expect(types).toContain(
|
|
877
|
-
expect(types).toContain(
|
|
990
|
+
expect(types).toContain("assistant_text_delta");
|
|
991
|
+
expect(types).toContain("message_complete");
|
|
878
992
|
|
|
879
993
|
const lastMsg = sent[sent.length - 1];
|
|
880
|
-
expect(lastMsg.type).toBe(
|
|
994
|
+
expect(lastMsg.type).toBe("message_complete");
|
|
881
995
|
});
|
|
882
996
|
|
|
883
|
-
test(
|
|
884
|
-
mockIntentResult = { kind:
|
|
885
|
-
mockExecuteResult = {
|
|
997
|
+
test("resume_only → executeRecordingIntent called, sends text_delta + message_complete, returns early", async () => {
|
|
998
|
+
mockIntentResult = { kind: "resume_only" };
|
|
999
|
+
mockExecuteResult = {
|
|
1000
|
+
handled: true,
|
|
1001
|
+
responseText: "Resuming the recording.",
|
|
1002
|
+
};
|
|
886
1003
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
887
1004
|
|
|
888
|
-
const { handleUserMessage } =
|
|
1005
|
+
const { handleUserMessage } =
|
|
1006
|
+
await import("../daemon/handlers/sessions.js");
|
|
889
1007
|
await handleUserMessage(
|
|
890
1008
|
{
|
|
891
|
-
type:
|
|
892
|
-
sessionId:
|
|
893
|
-
content:
|
|
894
|
-
interface:
|
|
1009
|
+
type: "user_message",
|
|
1010
|
+
sessionId: "test-session",
|
|
1011
|
+
content: "resume the recording",
|
|
1012
|
+
interface: "vellum",
|
|
895
1013
|
} as any,
|
|
896
1014
|
fakeSocket,
|
|
897
1015
|
ctx,
|
|
@@ -900,25 +1018,33 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
900
1018
|
expect(executorCalled).toBe(true);
|
|
901
1019
|
|
|
902
1020
|
const types = sent.map((m) => m.type);
|
|
903
|
-
expect(types).toContain(
|
|
904
|
-
expect(types).toContain(
|
|
1021
|
+
expect(types).toContain("assistant_text_delta");
|
|
1022
|
+
expect(types).toContain("message_complete");
|
|
905
1023
|
|
|
906
1024
|
const lastMsg = sent[sent.length - 1];
|
|
907
|
-
expect(lastMsg.type).toBe(
|
|
1025
|
+
expect(lastMsg.type).toBe("message_complete");
|
|
908
1026
|
});
|
|
909
1027
|
|
|
910
|
-
test(
|
|
911
|
-
mockIntentResult = {
|
|
912
|
-
|
|
1028
|
+
test("restart_with_remainder → defers restart, continues with remaining text", async () => {
|
|
1029
|
+
mockIntentResult = {
|
|
1030
|
+
kind: "restart_with_remainder",
|
|
1031
|
+
remainder: "open Safari",
|
|
1032
|
+
};
|
|
1033
|
+
mockExecuteResult = {
|
|
1034
|
+
handled: false,
|
|
1035
|
+
remainderText: "open Safari",
|
|
1036
|
+
pendingRestart: true,
|
|
1037
|
+
};
|
|
913
1038
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
914
1039
|
|
|
915
|
-
const { handleUserMessage } =
|
|
1040
|
+
const { handleUserMessage } =
|
|
1041
|
+
await import("../daemon/handlers/sessions.js");
|
|
916
1042
|
await handleUserMessage(
|
|
917
1043
|
{
|
|
918
|
-
type:
|
|
919
|
-
sessionId:
|
|
920
|
-
content:
|
|
921
|
-
interface:
|
|
1044
|
+
type: "user_message",
|
|
1045
|
+
sessionId: "test-session",
|
|
1046
|
+
content: "restart the recording and open Safari",
|
|
1047
|
+
interface: "vellum",
|
|
922
1048
|
} as any,
|
|
923
1049
|
fakeSocket,
|
|
924
1050
|
ctx,
|
|
@@ -929,24 +1055,27 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
929
1055
|
|
|
930
1056
|
// Should NOT have restart-specific intercept messages
|
|
931
1057
|
const recordingSpecific = sent.filter(
|
|
932
|
-
(m) =>
|
|
933
|
-
m.
|
|
1058
|
+
(m) =>
|
|
1059
|
+
m.type === "assistant_text_delta" &&
|
|
1060
|
+
typeof m.text === "string" &&
|
|
1061
|
+
m.text.includes("Restarting screen recording"),
|
|
934
1062
|
);
|
|
935
1063
|
expect(recordingSpecific).toHaveLength(0);
|
|
936
1064
|
});
|
|
937
1065
|
|
|
938
|
-
test(
|
|
939
|
-
mockIntentResult = { kind:
|
|
1066
|
+
test("commandIntent restart → routes directly via handleRecordingRestart, returns early", async () => {
|
|
1067
|
+
mockIntentResult = { kind: "none" };
|
|
940
1068
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
941
1069
|
|
|
942
|
-
const { handleUserMessage } =
|
|
1070
|
+
const { handleUserMessage } =
|
|
1071
|
+
await import("../daemon/handlers/sessions.js");
|
|
943
1072
|
await handleUserMessage(
|
|
944
1073
|
{
|
|
945
|
-
type:
|
|
946
|
-
sessionId:
|
|
947
|
-
content:
|
|
948
|
-
interface:
|
|
949
|
-
commandIntent: { domain:
|
|
1074
|
+
type: "user_message",
|
|
1075
|
+
sessionId: "test-session",
|
|
1076
|
+
content: "restart recording",
|
|
1077
|
+
interface: "vellum",
|
|
1078
|
+
commandIntent: { domain: "screen_recording", action: "restart" },
|
|
950
1079
|
} as any,
|
|
951
1080
|
fakeSocket,
|
|
952
1081
|
ctx,
|
|
@@ -955,25 +1084,26 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
955
1084
|
expect(recordingRestartCalled).toBe(true);
|
|
956
1085
|
|
|
957
1086
|
const types = sent.map((m) => m.type);
|
|
958
|
-
expect(types).toContain(
|
|
959
|
-
expect(types).toContain(
|
|
1087
|
+
expect(types).toContain("assistant_text_delta");
|
|
1088
|
+
expect(types).toContain("message_complete");
|
|
960
1089
|
|
|
961
1090
|
const lastMsg = sent[sent.length - 1];
|
|
962
|
-
expect(lastMsg.type).toBe(
|
|
1091
|
+
expect(lastMsg.type).toBe("message_complete");
|
|
963
1092
|
});
|
|
964
1093
|
|
|
965
|
-
test(
|
|
966
|
-
mockIntentResult = { kind:
|
|
1094
|
+
test("commandIntent pause → routes directly via handleRecordingPause, returns early", async () => {
|
|
1095
|
+
mockIntentResult = { kind: "none" };
|
|
967
1096
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
968
1097
|
|
|
969
|
-
const { handleUserMessage } =
|
|
1098
|
+
const { handleUserMessage } =
|
|
1099
|
+
await import("../daemon/handlers/sessions.js");
|
|
970
1100
|
await handleUserMessage(
|
|
971
1101
|
{
|
|
972
|
-
type:
|
|
973
|
-
sessionId:
|
|
974
|
-
content:
|
|
975
|
-
interface:
|
|
976
|
-
commandIntent: { domain:
|
|
1102
|
+
type: "user_message",
|
|
1103
|
+
sessionId: "test-session",
|
|
1104
|
+
content: "pause recording",
|
|
1105
|
+
interface: "vellum",
|
|
1106
|
+
commandIntent: { domain: "screen_recording", action: "pause" },
|
|
977
1107
|
} as any,
|
|
978
1108
|
fakeSocket,
|
|
979
1109
|
ctx,
|
|
@@ -982,22 +1112,23 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
982
1112
|
expect(recordingPauseCalled).toBe(true);
|
|
983
1113
|
|
|
984
1114
|
const types = sent.map((m) => m.type);
|
|
985
|
-
expect(types).toContain(
|
|
986
|
-
expect(types).toContain(
|
|
1115
|
+
expect(types).toContain("assistant_text_delta");
|
|
1116
|
+
expect(types).toContain("message_complete");
|
|
987
1117
|
});
|
|
988
1118
|
|
|
989
|
-
test(
|
|
990
|
-
mockIntentResult = { kind:
|
|
1119
|
+
test("commandIntent resume → routes directly via handleRecordingResume, returns early", async () => {
|
|
1120
|
+
mockIntentResult = { kind: "none" };
|
|
991
1121
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
992
1122
|
|
|
993
|
-
const { handleUserMessage } =
|
|
1123
|
+
const { handleUserMessage } =
|
|
1124
|
+
await import("../daemon/handlers/sessions.js");
|
|
994
1125
|
await handleUserMessage(
|
|
995
1126
|
{
|
|
996
|
-
type:
|
|
997
|
-
sessionId:
|
|
998
|
-
content:
|
|
999
|
-
interface:
|
|
1000
|
-
commandIntent: { domain:
|
|
1127
|
+
type: "user_message",
|
|
1128
|
+
sessionId: "test-session",
|
|
1129
|
+
content: "resume recording",
|
|
1130
|
+
interface: "vellum",
|
|
1131
|
+
commandIntent: { domain: "screen_recording", action: "resume" },
|
|
1001
1132
|
} as any,
|
|
1002
1133
|
fakeSocket,
|
|
1003
1134
|
ctx,
|
|
@@ -1006,7 +1137,7 @@ describe('recording intent handler integration — handleUserMessage', () => {
|
|
|
1006
1137
|
expect(recordingResumeCalled).toBe(true);
|
|
1007
1138
|
|
|
1008
1139
|
const types = sent.map((m) => m.type);
|
|
1009
|
-
expect(types).toContain(
|
|
1010
|
-
expect(types).toContain(
|
|
1140
|
+
expect(types).toContain("assistant_text_delta");
|
|
1141
|
+
expect(types).toContain("message_complete");
|
|
1011
1142
|
});
|
|
1012
1143
|
});
|