@vellumai/assistant 0.3.19 → 0.3.21
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 +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +10 -2
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -1,14 +1,42 @@
|
|
|
1
|
+
import { consumeGrantForInvocation } from '../approvals/approval-primitive.js';
|
|
1
2
|
import { isToolBlocked } from '../security/parental-control-store.js';
|
|
3
|
+
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
2
4
|
import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
|
|
3
5
|
import { getLogger } from '../util/logger.js';
|
|
4
6
|
import { enforceGuardianOnlyPolicy } from './guardian-control-plane-policy.js';
|
|
5
7
|
import { getAllTools, getTool } from './registry.js';
|
|
8
|
+
import { isSideEffectTool } from './side-effects.js';
|
|
6
9
|
import type { ExecutionTarget, Tool, ToolContext, ToolExecutionResult, ToolLifecycleEvent } from './types.js';
|
|
7
10
|
|
|
8
11
|
const log = getLogger('tool-approval-handler');
|
|
9
12
|
|
|
13
|
+
function isUntrustedGuardianActorRole(role: ToolContext['guardianActorRole']): boolean {
|
|
14
|
+
return role === 'non-guardian' || role === 'unverified_channel';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requiresGuardianApprovalForActor(
|
|
18
|
+
toolName: string,
|
|
19
|
+
input: Record<string, unknown>,
|
|
20
|
+
executionTarget: ExecutionTarget,
|
|
21
|
+
): boolean {
|
|
22
|
+
// Side-effect tools always require guardian approval for untrusted actors.
|
|
23
|
+
// Read-only host execution is also blocked because it can leak sensitive
|
|
24
|
+
// local information (e.g. shell/file reads).
|
|
25
|
+
return isSideEffectTool(toolName, input) || executionTarget === 'host';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function guardianApprovalDeniedMessage(
|
|
29
|
+
actorRole: ToolContext['guardianActorRole'],
|
|
30
|
+
toolName: string,
|
|
31
|
+
): string {
|
|
32
|
+
if (actorRole === 'unverified_channel') {
|
|
33
|
+
return `Permission denied for "${toolName}": this action requires guardian approval from a verified channel identity.`;
|
|
34
|
+
}
|
|
35
|
+
return `Permission denied for "${toolName}": this action requires guardian approval and the current actor is not the guardian.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
10
38
|
export type PreExecutionGateResult =
|
|
11
|
-
| { allowed: true; tool: Tool }
|
|
39
|
+
| { allowed: true; tool: Tool; grantConsumed?: boolean }
|
|
12
40
|
| { allowed: false; result: ToolExecutionResult };
|
|
13
41
|
|
|
14
42
|
/**
|
|
@@ -22,7 +50,7 @@ export class ToolApprovalHandler {
|
|
|
22
50
|
* Returns the resolved Tool if all gates pass, or an early-return
|
|
23
51
|
* ToolExecutionResult if any gate blocks execution.
|
|
24
52
|
*/
|
|
25
|
-
checkPreExecutionGates(
|
|
53
|
+
async checkPreExecutionGates(
|
|
26
54
|
name: string,
|
|
27
55
|
input: Record<string, unknown>,
|
|
28
56
|
context: ToolContext,
|
|
@@ -30,7 +58,7 @@ export class ToolApprovalHandler {
|
|
|
30
58
|
riskLevel: string,
|
|
31
59
|
startTime: number,
|
|
32
60
|
emitLifecycleEvent: (event: ToolLifecycleEvent) => void,
|
|
33
|
-
): PreExecutionGateResult {
|
|
61
|
+
): Promise<PreExecutionGateResult> {
|
|
34
62
|
// Bail out immediately if the session was aborted before this tool started.
|
|
35
63
|
if (context.signal?.aborted) {
|
|
36
64
|
const durationMs = Date.now() - startTime;
|
|
@@ -111,6 +139,33 @@ export class ToolApprovalHandler {
|
|
|
111
139
|
return { allowed: false, result: { content: guardianCheck.reason!, isError: true } };
|
|
112
140
|
}
|
|
113
141
|
|
|
142
|
+
// Determine whether this invocation requires a scoped grant. Capture
|
|
143
|
+
// the consume params now but defer the actual atomic consumption until
|
|
144
|
+
// after all downstream policy gates (allowedToolNames, task-run
|
|
145
|
+
// preflight, tool registry) pass. This prevents wasting a one-time-use
|
|
146
|
+
// grant when a subsequent gate rejects the invocation.
|
|
147
|
+
let needsGrantConsumption = false;
|
|
148
|
+
let deferredConsumeParams: Parameters<typeof consumeGrantForInvocation>[0] | null = null;
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
isUntrustedGuardianActorRole(context.guardianActorRole)
|
|
152
|
+
&& requiresGuardianApprovalForActor(name, input, executionTarget)
|
|
153
|
+
) {
|
|
154
|
+
const inputDigest = computeToolApprovalDigest(name, input);
|
|
155
|
+
needsGrantConsumption = true;
|
|
156
|
+
deferredConsumeParams = {
|
|
157
|
+
requestId: context.requestId,
|
|
158
|
+
toolName: name,
|
|
159
|
+
inputDigest,
|
|
160
|
+
consumingRequestId: context.requestId ?? `preexec-${context.sessionId}-${Date.now()}`,
|
|
161
|
+
assistantId: context.assistantId ?? 'self',
|
|
162
|
+
executionChannel: context.executionChannel,
|
|
163
|
+
conversationId: context.conversationId,
|
|
164
|
+
callSessionId: context.callSessionId,
|
|
165
|
+
requesterExternalUserId: context.requesterExternalUserId,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
114
169
|
// Gate tools not active for the current turn
|
|
115
170
|
if (context.allowedToolNames && !context.allowedToolNames.has(name)) {
|
|
116
171
|
const msg = `Tool "${name}" is not currently active. Load the skill that provides this tool first.`;
|
|
@@ -187,6 +242,89 @@ export class ToolApprovalHandler {
|
|
|
187
242
|
return { allowed: false, result: { content: msg, isError: true } };
|
|
188
243
|
}
|
|
189
244
|
|
|
245
|
+
// All policy gates passed. Now consume the scoped grant if one is
|
|
246
|
+
// required. Deferring consumption to this point ensures a downstream
|
|
247
|
+
// rejection (allowedToolNames, task-run preflight, registry lookup)
|
|
248
|
+
// does not waste the one-time-use grant.
|
|
249
|
+
//
|
|
250
|
+
// Retry polling is scoped to the voice channel where a race condition
|
|
251
|
+
// exists between fire-and-forget turn execution and LLM fallback grant
|
|
252
|
+
// minting (2-5s). Non-voice channels get an instant sync lookup so
|
|
253
|
+
// normal denials are not delayed.
|
|
254
|
+
if (needsGrantConsumption && deferredConsumeParams) {
|
|
255
|
+
const isVoice = context.executionChannel === 'voice';
|
|
256
|
+
const grantResult = await consumeGrantForInvocation(
|
|
257
|
+
deferredConsumeParams,
|
|
258
|
+
isVoice ? { signal: context.signal } : { maxWaitMs: 0 },
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (grantResult.ok) {
|
|
262
|
+
log.info({
|
|
263
|
+
toolName: name,
|
|
264
|
+
sessionId: context.sessionId,
|
|
265
|
+
conversationId: context.conversationId,
|
|
266
|
+
actorRole: context.guardianActorRole,
|
|
267
|
+
executionTarget,
|
|
268
|
+
grantId: grantResult.grant.id,
|
|
269
|
+
}, 'Scoped grant consumed — allowing untrusted actor tool invocation');
|
|
270
|
+
|
|
271
|
+
return { allowed: true, tool, grantConsumed: true };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Treat abort as a cancellation — not a grant denial. This matches
|
|
275
|
+
// the abort check at the top of checkPreExecutionGates so the caller
|
|
276
|
+
// sees a consistent "Cancelled" result instead of a spurious
|
|
277
|
+
// guardian_approval_required denial during voice barge-in.
|
|
278
|
+
if (grantResult.reason === 'aborted') {
|
|
279
|
+
const durationMs = Date.now() - startTime;
|
|
280
|
+
emitLifecycleEvent({
|
|
281
|
+
type: 'error',
|
|
282
|
+
toolName: name,
|
|
283
|
+
executionTarget,
|
|
284
|
+
input,
|
|
285
|
+
workingDir: context.workingDir,
|
|
286
|
+
sessionId: context.sessionId,
|
|
287
|
+
conversationId: context.conversationId,
|
|
288
|
+
requestId: context.requestId,
|
|
289
|
+
riskLevel,
|
|
290
|
+
decision: 'error',
|
|
291
|
+
durationMs,
|
|
292
|
+
errorMessage: 'Cancelled',
|
|
293
|
+
isExpected: true,
|
|
294
|
+
errorCategory: 'tool_failure',
|
|
295
|
+
});
|
|
296
|
+
return { allowed: false, result: { content: 'Cancelled', isError: true } };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// No matching grant or race condition — deny.
|
|
300
|
+
const reason = guardianApprovalDeniedMessage(context.guardianActorRole, name);
|
|
301
|
+
log.warn({
|
|
302
|
+
toolName: name,
|
|
303
|
+
sessionId: context.sessionId,
|
|
304
|
+
conversationId: context.conversationId,
|
|
305
|
+
actorRole: context.guardianActorRole,
|
|
306
|
+
executionTarget,
|
|
307
|
+
reason: 'guardian_approval_required',
|
|
308
|
+
grantMissReason: grantResult.reason,
|
|
309
|
+
}, 'Guardian approval gate blocked untrusted actor tool invocation (no matching grant)');
|
|
310
|
+
const durationMs = Date.now() - startTime;
|
|
311
|
+
emitLifecycleEvent({
|
|
312
|
+
type: 'permission_denied',
|
|
313
|
+
toolName: name,
|
|
314
|
+
executionTarget,
|
|
315
|
+
input,
|
|
316
|
+
workingDir: context.workingDir,
|
|
317
|
+
sessionId: context.sessionId,
|
|
318
|
+
conversationId: context.conversationId,
|
|
319
|
+
requestId: context.requestId,
|
|
320
|
+
riskLevel,
|
|
321
|
+
decision: 'deny',
|
|
322
|
+
reason,
|
|
323
|
+
durationMs,
|
|
324
|
+
});
|
|
325
|
+
return { allowed: false, result: { content: reason, isError: true } };
|
|
326
|
+
}
|
|
327
|
+
|
|
190
328
|
return { allowed: true, tool };
|
|
191
329
|
}
|
|
192
330
|
}
|
|
@@ -12,6 +12,9 @@ import { credentialStoreTool } from './credentials/vault.js';
|
|
|
12
12
|
import { memorySaveTool, memorySearchTool, memoryUpdateTool } from './memory/register.js';
|
|
13
13
|
import type { LazyToolDescriptor } from './registry.js';
|
|
14
14
|
import { vellumSkillsCatalogTool } from './skills/vellum-catalog.js';
|
|
15
|
+
import { setAvatarTool } from './system/avatar-generator.js';
|
|
16
|
+
import { navigateSettingsTabTool } from './system/navigate-settings.js';
|
|
17
|
+
import { openSystemSettingsTool } from './system/open-system-settings.js';
|
|
15
18
|
import { voiceConfigUpdateTool } from './system/voice-config.js';
|
|
16
19
|
import type { Tool } from './types.js';
|
|
17
20
|
import { screenWatchTool } from './watch/screen-watch.js';
|
|
@@ -68,6 +71,9 @@ export const explicitTools: Tool[] = [
|
|
|
68
71
|
screenWatchTool,
|
|
69
72
|
vellumSkillsCatalogTool,
|
|
70
73
|
voiceConfigUpdateTool,
|
|
74
|
+
setAvatarTool,
|
|
75
|
+
openSystemSettingsTool,
|
|
76
|
+
navigateSettingsTabTool,
|
|
71
77
|
];
|
|
72
78
|
|
|
73
79
|
// ── Lazy tool descriptors ───────────────────────────────────────────
|
package/src/tools/types.ts
CHANGED
|
@@ -138,6 +138,12 @@ export interface ToolContext {
|
|
|
138
138
|
principal?: string;
|
|
139
139
|
/** Guardian actor role for the session — used by the guardian control-plane policy gate. */
|
|
140
140
|
guardianActorRole?: 'guardian' | 'non-guardian' | 'unverified_channel';
|
|
141
|
+
/** Channel through which the tool invocation originates (e.g. 'telegram', 'voice'). Used for scoped grant consumption. */
|
|
142
|
+
executionChannel?: string;
|
|
143
|
+
/** Voice/call session ID, if the invocation originates from a call. Used for scoped grant consumption. */
|
|
144
|
+
callSessionId?: string;
|
|
145
|
+
/** External user ID of the requester (non-guardian actor). Used for scoped grant consumption. */
|
|
146
|
+
requesterExternalUserId?: string;
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
export interface DiffInfo {
|
|
@@ -164,10 +170,12 @@ export interface Tool {
|
|
|
164
170
|
defaultRiskLevel: RiskLevel;
|
|
165
171
|
/** When set to 'proxy', the tool is forwarded to a connected client rather than executed locally. */
|
|
166
172
|
executionMode?: 'local' | 'proxy';
|
|
167
|
-
/** Whether this tool is a core built-in
|
|
168
|
-
origin?: 'core' | 'skill';
|
|
173
|
+
/** Whether this tool is a core built-in, provided by a skill, or from an MCP server. */
|
|
174
|
+
origin?: 'core' | 'skill' | 'mcp';
|
|
169
175
|
/** If origin is 'skill', the ID of the owning skill. */
|
|
170
176
|
ownerSkillId?: string;
|
|
177
|
+
/** If origin is 'mcp', the ID of the owning MCP server. */
|
|
178
|
+
ownerMcpServerId?: string;
|
|
171
179
|
/** Content-hash of the owning skill's source at registration time. */
|
|
172
180
|
ownerSkillVersionHash?: string;
|
|
173
181
|
/** Whether the owning skill is bundled with the daemon (trusted first-party). */
|
package/src/util/diff.ts
CHANGED
|
@@ -54,7 +54,7 @@ interface Hunk {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const CONTEXT_LINES = 3;
|
|
57
|
-
const
|
|
57
|
+
const DEFAULT_MAX_EXACT_DIFF_LINES = 1000;
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Group diff entries into hunks with surrounding context lines.
|
|
@@ -108,24 +108,44 @@ const CYAN = '\x1b[36m';
|
|
|
108
108
|
const DIM = '\x1b[2m';
|
|
109
109
|
const RESET = '\x1b[0m';
|
|
110
110
|
|
|
111
|
+
export interface FormatDiffOptions {
|
|
112
|
+
maxExactLines?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatLargeDiffFallback(oldLines: string[], newLines: string[], filePath: string): string {
|
|
116
|
+
let output = `${DIM}--- a/${filePath}${RESET}\n`;
|
|
117
|
+
output += `${DIM}+++ b/${filePath}${RESET}\n`;
|
|
118
|
+
output += `${CYAN}@@ -1,${oldLines.length} +1,${newLines.length} @@${RESET}\n`;
|
|
119
|
+
|
|
120
|
+
for (const line of oldLines) {
|
|
121
|
+
output += `${RED}-${line}${RESET}\n`;
|
|
122
|
+
}
|
|
123
|
+
for (const line of newLines) {
|
|
124
|
+
output += `${GREEN}+${line}${RESET}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
|
|
111
130
|
/**
|
|
112
131
|
* Format a colored unified diff from old and new file content.
|
|
113
132
|
* Returns an empty string if the contents are identical.
|
|
114
133
|
*/
|
|
115
|
-
export function formatDiff(
|
|
134
|
+
export function formatDiff(
|
|
135
|
+
oldContent: string,
|
|
136
|
+
newContent: string,
|
|
137
|
+
filePath: string,
|
|
138
|
+
options: FormatDiffOptions = {},
|
|
139
|
+
): string {
|
|
116
140
|
if (oldContent === newContent) return '';
|
|
117
141
|
|
|
118
142
|
const oldLines = oldContent.split('\n');
|
|
119
143
|
const newLines = newContent.split('\n');
|
|
144
|
+
const maxExactLines = options.maxExactLines ?? DEFAULT_MAX_EXACT_DIFF_LINES;
|
|
120
145
|
|
|
121
146
|
// Guard against quadratic blowup on large files
|
|
122
|
-
if (oldLines.length >
|
|
123
|
-
|
|
124
|
-
const added = newLines.length;
|
|
125
|
-
let output = `${DIM}--- a/${filePath}${RESET}\n`;
|
|
126
|
-
output += `${DIM}+++ b/${filePath}${RESET}\n`;
|
|
127
|
-
output += `${DIM}[Diff too large to display: ${removed} lines → ${added} lines]${RESET}\n`;
|
|
128
|
-
return output;
|
|
147
|
+
if (oldLines.length > maxExactLines || newLines.length > maxExactLines) {
|
|
148
|
+
return formatLargeDiffFallback(oldLines, newLines, filePath);
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
const entries = computeLineDiff(oldLines, newLines);
|
|
@@ -159,11 +179,14 @@ export function formatDiff(oldContent: string, newContent: string, filePath: str
|
|
|
159
179
|
/**
|
|
160
180
|
* Format a "new file" diff (everything is added).
|
|
161
181
|
* Truncates to maxLines to avoid flooding the terminal.
|
|
182
|
+
* Pass `null` for unbounded output.
|
|
162
183
|
*/
|
|
163
|
-
export function formatNewFileDiff(content: string, filePath: string, maxLines = 20): string {
|
|
184
|
+
export function formatNewFileDiff(content: string, filePath: string, maxLines: number | null = 20): string {
|
|
164
185
|
const lines = content.split('\n');
|
|
165
|
-
const
|
|
166
|
-
const
|
|
186
|
+
const shouldTruncate = typeof maxLines === 'number' && Number.isFinite(maxLines);
|
|
187
|
+
const boundedMaxLines = shouldTruncate ? Math.max(0, Math.floor(maxLines)) : lines.length;
|
|
188
|
+
const truncated = lines.length > boundedMaxLines;
|
|
189
|
+
const displayLines = truncated ? lines.slice(0, boundedMaxLines) : lines;
|
|
167
190
|
|
|
168
191
|
let output = `${DIM}--- /dev/null${RESET}\n`;
|
|
169
192
|
output += `${DIM}+++ b/${filePath}${RESET}\n`;
|
|
@@ -174,7 +197,7 @@ export function formatNewFileDiff(content: string, filePath: string, maxLines =
|
|
|
174
197
|
}
|
|
175
198
|
|
|
176
199
|
if (truncated) {
|
|
177
|
-
output += `${DIM}... ${lines.length -
|
|
200
|
+
output += `${DIM}... ${lines.length - boundedMaxLines} more lines${RESET}\n`;
|
|
178
201
|
}
|
|
179
202
|
|
|
180
203
|
return output;
|
package/Dockerfile.sandbox
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { describe, expect,it } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { SessionExpiredError } from '../doordash/client.js';
|
|
4
|
-
|
|
5
|
-
describe('SessionExpiredError', () => {
|
|
6
|
-
it('is an instance of Error', () => {
|
|
7
|
-
const err = new SessionExpiredError('test reason');
|
|
8
|
-
expect(err).toBeInstanceOf(Error);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('has name set to SessionExpiredError', () => {
|
|
12
|
-
const err = new SessionExpiredError('test reason');
|
|
13
|
-
expect(err.name).toBe('SessionExpiredError');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('preserves the reason as the message', () => {
|
|
17
|
-
const err = new SessionExpiredError('DoorDash session has expired.');
|
|
18
|
-
expect(err.message).toBe('DoorDash session has expired.');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('can be distinguished from plain Error via instanceof', () => {
|
|
22
|
-
const sessionErr = new SessionExpiredError('expired');
|
|
23
|
-
const plainErr = new Error('something else');
|
|
24
|
-
expect(sessionErr instanceof SessionExpiredError).toBe(true);
|
|
25
|
-
expect(plainErr instanceof SessionExpiredError).toBe(false);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('produces a useful stack trace', () => {
|
|
29
|
-
const err = new SessionExpiredError('no session');
|
|
30
|
-
expect(err.stack).toBeDefined();
|
|
31
|
-
expect(err.stack).toContain('SessionExpiredError');
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe('expired session classification', () => {
|
|
36
|
-
// The CDP response handler in cdpFetch classifies certain HTTP statuses
|
|
37
|
-
// as session-expired. We test the classification logic by simulating
|
|
38
|
-
// the parsed response structure that cdpFetch evaluates.
|
|
39
|
-
|
|
40
|
-
function classifyResponse(parsed: Record<string, unknown>): Error {
|
|
41
|
-
// Mirrors the classification logic from cdpFetch (client.ts lines 154-159)
|
|
42
|
-
if (parsed.__error) {
|
|
43
|
-
if (parsed.__status === 403 || parsed.__status === 401) {
|
|
44
|
-
return new SessionExpiredError('DoorDash session has expired.');
|
|
45
|
-
}
|
|
46
|
-
return new Error(
|
|
47
|
-
(parsed.__message as string) ??
|
|
48
|
-
`HTTP ${parsed.__status}: ${(parsed.__body as string) ?? ''}`,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
return new Error('No error');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
it('classifies HTTP 401 as SessionExpiredError', () => {
|
|
55
|
-
const err = classifyResponse({ __error: true, __status: 401, __body: 'Unauthorized' });
|
|
56
|
-
expect(err).toBeInstanceOf(SessionExpiredError);
|
|
57
|
-
expect(err.message).toBe('DoorDash session has expired.');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('classifies HTTP 403 as SessionExpiredError', () => {
|
|
61
|
-
const err = classifyResponse({ __error: true, __status: 403, __body: 'Forbidden' });
|
|
62
|
-
expect(err).toBeInstanceOf(SessionExpiredError);
|
|
63
|
-
expect(err.message).toBe('DoorDash session has expired.');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('classifies HTTP 500 as a generic Error, not session expired', () => {
|
|
67
|
-
const err = classifyResponse({ __error: true, __status: 500, __body: 'Internal Server Error' });
|
|
68
|
-
expect(err).not.toBeInstanceOf(SessionExpiredError);
|
|
69
|
-
expect(err.message).toBe('HTTP 500: Internal Server Error');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('classifies HTTP 429 as a generic Error', () => {
|
|
73
|
-
const err = classifyResponse({ __error: true, __status: 429, __body: 'Rate limited' });
|
|
74
|
-
expect(err).not.toBeInstanceOf(SessionExpiredError);
|
|
75
|
-
expect(err.message).toBe('HTTP 429: Rate limited');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('uses __message when available', () => {
|
|
79
|
-
const err = classifyResponse({ __error: true, __message: 'fetch failed' });
|
|
80
|
-
expect(err).not.toBeInstanceOf(SessionExpiredError);
|
|
81
|
-
expect(err.message).toBe('fetch failed');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('handles response with no __body or __message gracefully', () => {
|
|
85
|
-
const err = classifyResponse({ __error: true, __status: 502 });
|
|
86
|
-
expect(err).not.toBeInstanceOf(SessionExpiredError);
|
|
87
|
-
expect(err.message).toBe('HTTP 502: ');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('CDP failure scenarios', () => {
|
|
92
|
-
// These test the error conditions that cdpFetch can encounter:
|
|
93
|
-
// 1. CDP protocol error (msg.error present)
|
|
94
|
-
// 2. Empty CDP response (no value in result)
|
|
95
|
-
// 3. Timeout (30s)
|
|
96
|
-
// 4. WebSocket connection failure
|
|
97
|
-
|
|
98
|
-
// We can test the error construction logic without connecting to a real CDP
|
|
99
|
-
|
|
100
|
-
it('CDP protocol error produces a descriptive message', () => {
|
|
101
|
-
// Simulates the error path at client.ts line 143
|
|
102
|
-
const cdpError = { message: 'Cannot find context with specified id' };
|
|
103
|
-
const err = new Error(`CDP error: ${cdpError.message}`);
|
|
104
|
-
expect(err.message).toBe('CDP error: Cannot find context with specified id');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('Empty CDP response produces a clear error', () => {
|
|
108
|
-
// Simulates the error path at client.ts line 149
|
|
109
|
-
const value = undefined;
|
|
110
|
-
const err = !value ? new Error('Empty CDP response') : null;
|
|
111
|
-
expect(err).not.toBeNull();
|
|
112
|
-
expect(err!.message).toBe('Empty CDP response');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('CDP timeout error message includes the timeout duration', () => {
|
|
116
|
-
// Simulates the timeout error at client.ts line 92
|
|
117
|
-
const err = new Error('CDP fetch timed out after 30s');
|
|
118
|
-
expect(err.message).toContain('30s');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('WebSocket connection failure produces SessionExpiredError', () => {
|
|
122
|
-
// Simulates ws.onerror at client.ts line 172
|
|
123
|
-
const err = new SessionExpiredError('CDP connection failed.');
|
|
124
|
-
expect(err).toBeInstanceOf(SessionExpiredError);
|
|
125
|
-
expect(err.message).toBe('CDP connection failed.');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('findDoordashTab failure when CDP is unavailable', () => {
|
|
129
|
-
// Simulates findDoordashTab at client.ts line 67
|
|
130
|
-
const err = new SessionExpiredError(
|
|
131
|
-
'Chrome CDP not available. Run `vellum doordash refresh` first.',
|
|
132
|
-
);
|
|
133
|
-
expect(err).toBeInstanceOf(SessionExpiredError);
|
|
134
|
-
expect(err.message).toContain('Chrome CDP not available');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('findDoordashTab failure when no tab is available', () => {
|
|
138
|
-
// Simulates findDoordashTab at client.ts line 76
|
|
139
|
-
const err = new SessionExpiredError(
|
|
140
|
-
'No Chrome tab available for DoorDash requests.',
|
|
141
|
-
);
|
|
142
|
-
expect(err).toBeInstanceOf(SessionExpiredError);
|
|
143
|
-
expect(err.message).toContain('No Chrome tab available');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('requireSession throws SessionExpiredError when no session exists', () => {
|
|
147
|
-
// Simulates requireSession at client.ts line 56
|
|
148
|
-
const session = null;
|
|
149
|
-
const err = !session
|
|
150
|
-
? new SessionExpiredError('No DoorDash session found.')
|
|
151
|
-
: null;
|
|
152
|
-
expect(err).toBeInstanceOf(SessionExpiredError);
|
|
153
|
-
expect(err!.message).toBe('No DoorDash session found.');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('GraphQL errors are joined with semicolons', () => {
|
|
157
|
-
// Simulates the error handling at client.ts lines 192-194
|
|
158
|
-
const errors = [
|
|
159
|
-
{ message: 'Field "x" not found' },
|
|
160
|
-
{ message: 'Unauthorized' },
|
|
161
|
-
];
|
|
162
|
-
const msgs = errors.map(e => e.message || JSON.stringify(e)).join('; ');
|
|
163
|
-
const err = new Error(`GraphQL errors: ${msgs}`);
|
|
164
|
-
expect(err.message).toBe(
|
|
165
|
-
'GraphQL errors: Field "x" not found; Unauthorized',
|
|
166
|
-
);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('GraphQL errors use JSON.stringify for errors without message', () => {
|
|
170
|
-
const errors = [{ extensions: { code: 'INTERNAL_ERROR' } }];
|
|
171
|
-
const msgs = errors
|
|
172
|
-
.map(e => (e as Record<string, unknown>).message || JSON.stringify(e))
|
|
173
|
-
.join('; ');
|
|
174
|
-
const err = new Error(`GraphQL errors: ${msgs}`);
|
|
175
|
-
expect(err.message).toContain('INTERNAL_ERROR');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('Empty GraphQL response throws', () => {
|
|
179
|
-
// Simulates client.ts lines 196-198
|
|
180
|
-
const data = undefined;
|
|
181
|
-
const err = !data
|
|
182
|
-
? new Error('Empty response from DoorDash API')
|
|
183
|
-
: null;
|
|
184
|
-
expect(err).not.toBeNull();
|
|
185
|
-
expect(err!.message).toBe('Empty response from DoorDash API');
|
|
186
|
-
});
|
|
187
|
-
});
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync,writeFileSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { afterEach,beforeEach, describe, expect, it } from 'bun:test';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type DoorDashSession,
|
|
9
|
-
getCookieHeader,
|
|
10
|
-
getCsrfToken,
|
|
11
|
-
importFromRecording,
|
|
12
|
-
} from '../doordash/session.js';
|
|
13
|
-
|
|
14
|
-
// Override getDataDir to use a temp directory during tests
|
|
15
|
-
const TEST_DIR = join(tmpdir(), `vellum-dd-test-${process.pid}`);
|
|
16
|
-
let originalDataDir: string | undefined;
|
|
17
|
-
|
|
18
|
-
// We mock getDataDir by patching the module at the fs level:
|
|
19
|
-
// session.ts calls getSessionDir() -> join(getDataDir(), 'doordash')
|
|
20
|
-
// We'll test session.ts helpers that don't depend on getDataDir directly,
|
|
21
|
-
// and test the persistence functions via the actual file system with a known path.
|
|
22
|
-
|
|
23
|
-
function makeCookie(name: string, value: string): {
|
|
24
|
-
name: string;
|
|
25
|
-
value: string;
|
|
26
|
-
domain: string;
|
|
27
|
-
path: string;
|
|
28
|
-
httpOnly: boolean;
|
|
29
|
-
secure: boolean;
|
|
30
|
-
} {
|
|
31
|
-
return { name, value, domain: '.doordash.com', path: '/', httpOnly: false, secure: false };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function makeSession(overrides?: Partial<DoorDashSession>): DoorDashSession {
|
|
35
|
-
return {
|
|
36
|
-
cookies: [
|
|
37
|
-
makeCookie('dd_session', 'abc123'),
|
|
38
|
-
makeCookie('csrf_token', 'tok456'),
|
|
39
|
-
],
|
|
40
|
-
importedAt: '2025-01-15T12:00:00.000Z',
|
|
41
|
-
recordingId: 'rec-001',
|
|
42
|
-
...overrides,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe('DoorDash session helpers', () => {
|
|
47
|
-
describe('getCookieHeader', () => {
|
|
48
|
-
it('joins all cookies into a single header string', () => {
|
|
49
|
-
const session = makeSession();
|
|
50
|
-
const header = getCookieHeader(session);
|
|
51
|
-
expect(header).toBe('dd_session=abc123; csrf_token=tok456');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('returns empty string for a session with no cookies', () => {
|
|
55
|
-
const session = makeSession({ cookies: [] });
|
|
56
|
-
expect(getCookieHeader(session)).toBe('');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('handles a single cookie without trailing semicolons', () => {
|
|
60
|
-
const session = makeSession({ cookies: [makeCookie('a', '1')] });
|
|
61
|
-
expect(getCookieHeader(session)).toBe('a=1');
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('getCsrfToken', () => {
|
|
66
|
-
it('extracts the csrf_token value when present', () => {
|
|
67
|
-
const session = makeSession();
|
|
68
|
-
expect(getCsrfToken(session)).toBe('tok456');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('returns undefined when csrf_token is absent', () => {
|
|
72
|
-
const session = makeSession({
|
|
73
|
-
cookies: [makeCookie('dd_session', 'abc123')],
|
|
74
|
-
});
|
|
75
|
-
expect(getCsrfToken(session)).toBeUndefined();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('DoorDash session persistence', () => {
|
|
81
|
-
// These tests exercise the real loadSession/saveSession/clearSession
|
|
82
|
-
// by writing to the actual session path. We need to mock getDataDir.
|
|
83
|
-
// Since the module uses a private function we can't easily mock,
|
|
84
|
-
// we test via importFromRecording which exercises save+load.
|
|
85
|
-
|
|
86
|
-
beforeEach(() => {
|
|
87
|
-
originalDataDir = process.env.BASE_DATA_DIR;
|
|
88
|
-
process.env.BASE_DATA_DIR = TEST_DIR;
|
|
89
|
-
// Ensure test dir exists
|
|
90
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
afterEach(() => {
|
|
94
|
-
// Restore original BASE_DATA_DIR
|
|
95
|
-
if (originalDataDir === undefined) {
|
|
96
|
-
delete process.env.BASE_DATA_DIR;
|
|
97
|
-
} else {
|
|
98
|
-
process.env.BASE_DATA_DIR = originalDataDir;
|
|
99
|
-
}
|
|
100
|
-
// Clean up test dir
|
|
101
|
-
if (existsSync(TEST_DIR)) {
|
|
102
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('importFromRecording', () => {
|
|
107
|
-
it('throws when the recording file does not exist', () => {
|
|
108
|
-
expect(() => importFromRecording('/nonexistent/recording.json')).toThrow(
|
|
109
|
-
'Recording not found',
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('throws when the recording contains no cookies', () => {
|
|
114
|
-
const recordingPath = join(TEST_DIR, 'empty-recording.json');
|
|
115
|
-
writeFileSync(
|
|
116
|
-
recordingPath,
|
|
117
|
-
JSON.stringify({
|
|
118
|
-
id: 'rec-empty',
|
|
119
|
-
startedAt: 0,
|
|
120
|
-
endedAt: 1,
|
|
121
|
-
targetDomain: 'doordash.com',
|
|
122
|
-
networkEntries: [],
|
|
123
|
-
cookies: [],
|
|
124
|
-
observations: [],
|
|
125
|
-
}),
|
|
126
|
-
);
|
|
127
|
-
expect(() => importFromRecording(recordingPath)).toThrow(
|
|
128
|
-
'Recording contains no cookies',
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('successfully imports a recording with cookies', () => {
|
|
133
|
-
const recordingPath = join(TEST_DIR, 'valid-recording.json');
|
|
134
|
-
writeFileSync(
|
|
135
|
-
recordingPath,
|
|
136
|
-
JSON.stringify({
|
|
137
|
-
id: 'rec-valid',
|
|
138
|
-
startedAt: 0,
|
|
139
|
-
endedAt: 1,
|
|
140
|
-
targetDomain: 'doordash.com',
|
|
141
|
-
networkEntries: [],
|
|
142
|
-
cookies: [makeCookie('session_id', 'xyz')],
|
|
143
|
-
observations: [],
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
const session = importFromRecording(recordingPath);
|
|
147
|
-
expect(session.cookies).toHaveLength(1);
|
|
148
|
-
expect(session.cookies[0].name).toBe('session_id');
|
|
149
|
-
expect(session.cookies[0].value).toBe('xyz');
|
|
150
|
-
expect(session.recordingId).toBe('rec-valid');
|
|
151
|
-
expect(session.importedAt).toBeTruthy();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
});
|