@vellumai/assistant 0.11.10 → 0.11.11-staging.1
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/AGENTS.md +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/openapi.yaml +152 -55
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +60 -0
- package/src/__tests__/config-get-vision-flag.test.ts +36 -0
- package/src/__tests__/conversation-error.test.ts +38 -0
- package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
- package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
- package/src/__tests__/headless-browser-mode.test.ts +63 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +118 -27
- package/src/__tests__/host-app-control-proxy.test.ts +2 -2
- package/src/__tests__/host-app-control-routes.test.ts +41 -0
- package/src/__tests__/host-bash-routes.test.ts +30 -1
- package/src/__tests__/host-browser-routes.test.ts +34 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-proxy-base.test.ts +1 -0
- package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
- package/src/__tests__/llm-schema.test.ts +32 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
- package/src/__tests__/memory-config-file-parity.test.ts +18 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
- package/src/__tests__/messaging-send-tool.test.ts +159 -13
- package/src/__tests__/messaging-shared.test.ts +19 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
- package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
- package/src/__tests__/schedule-routes.test.ts +30 -1
- package/src/__tests__/scheduler-result-notification.test.ts +209 -0
- package/src/__tests__/skills-install-extract.test.ts +26 -0
- package/src/__tests__/skills-install-staging.test.ts +11 -2
- package/src/api/constants/personality-sliders.test.ts +21 -0
- package/src/api/constants/personality-sliders.ts +64 -0
- package/src/api/index.ts +8 -0
- package/src/archive/ustar-size.test.ts +32 -0
- package/src/archive/ustar-size.ts +23 -0
- package/src/background-wake/next-wake.test.ts +13 -0
- package/src/background-wake/next-wake.ts +4 -2
- package/src/calls/voice-session-bridge.ts +26 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
- package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
- package/src/cli/commands/config.help.ts +3 -1
- package/src/cli/commands/memory/worker.ts +1 -1
- package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
- package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
- package/src/cli/commands/platform/credits-format.ts +57 -0
- package/src/cli/commands/platform/index.help.ts +19 -3
- package/src/cli/commands/plugins.help.ts +4 -1
- package/src/cli/commands/plugins.ts +2 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-platform.ts +8 -10
- package/src/config/bundled-skills/messaging/SKILL.md +2 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +9 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/input-modalities.test.ts +71 -0
- package/src/config/input-modalities.ts +95 -0
- package/src/config/schemas/heartbeat.ts +3 -3
- package/src/config/schemas/llm.ts +13 -3
- package/src/config/schemas/memory-storage.test.ts +39 -0
- package/src/config/schemas/memory-storage.ts +21 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
- package/src/daemon/conversation-error.ts +20 -7
- package/src/daemon/embedding-reconcile.ts +64 -1
- package/src/daemon/handlers/config-embeddings.ts +31 -0
- package/src/daemon/host-app-control-proxy.ts +2 -0
- package/src/daemon/host-bash-proxy.ts +1 -0
- package/src/daemon/host-cu-proxy.ts +6 -6
- package/src/daemon/host-file-proxy.ts +6 -6
- package/src/daemon/host-proxy-base.ts +10 -9
- package/src/daemon/host-transfer-proxy.ts +21 -24
- package/src/daemon/mcp-reload-service.ts +6 -0
- package/src/daemon/providers-setup.ts +5 -26
- package/src/heartbeat/heartbeat-service.ts +72 -13
- package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
- package/src/live-voice/live-voice-photo.ts +68 -4
- package/src/live-voice/live-voice-session.ts +36 -2
- package/src/live-voice/protocol.ts +98 -1
- package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
- package/src/mcp/__tests__/reload-signal.test.ts +57 -0
- package/src/mcp/__tests__/startup.test.ts +150 -0
- package/src/mcp/reload-signal.ts +50 -0
- package/src/mcp/startup.ts +101 -0
- package/src/messaging/providers/outlook/adapter.ts +2 -13
- package/src/messaging/providers/outlook/client.ts +13 -1
- package/src/messaging/providers/outlook/types.ts +3 -0
- package/src/notifications/__tests__/decision-engine.test.ts +79 -6
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
- package/src/notifications/copy-composer.ts +33 -11
- package/src/notifications/decision-engine.ts +48 -11
- package/src/notifications/events-store.ts +68 -2
- package/src/notifications/home-feed-side-effect.ts +15 -3
- package/src/notifications/schedule-result-producer.ts +275 -0
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/seed-providers.ts +3 -2
- package/src/persistence/conversation-crud.ts +38 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
- package/src/persistence/embeddings/embedding-backend.ts +148 -11
- package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
- package/src/persistence/embeddings/embedding-openai.ts +55 -4
- package/src/persistence/embeddings/embedding-types.test.ts +24 -0
- package/src/persistence/embeddings/embedding-types.ts +25 -1
- package/src/persistence/job-utils.ts +6 -2
- package/src/plugin-api/index.ts +2 -1
- package/src/plugin-api/vision-support.test.ts +81 -0
- package/src/plugin-api/vision-support.ts +57 -10
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
- package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
- package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
- package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
- package/src/providers/anthropic/client.ts +9 -4
- package/src/providers/inline-audio-support.test.ts +34 -0
- package/src/providers/inline-audio-support.ts +27 -0
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
- package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
- package/src/providers/openai/api-error-normalization.ts +18 -3
- package/src/providers/openai/chat-completions-provider.ts +113 -13
- package/src/providers/openai/responses-provider.ts +10 -8
- package/src/providers/provider-secret-catalog.ts +11 -1
- package/src/providers/vellum/client.ts +14 -1
- package/src/providers/vellum/personality-directions.test.ts +163 -0
- package/src/providers/vellum/personality-directions.ts +120 -0
- package/src/providers/xml-tool-call-salvage.ts +212 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
- package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
- package/src/runtime/auth/same-actor.ts +29 -5
- package/src/runtime/background-job-runner.ts +38 -29
- package/src/runtime/guardian-action-service.ts +20 -1
- package/src/runtime/migrations/vbundle-validator.ts +3 -3
- package/src/runtime/pending-interactions.ts +4 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
- package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
- package/src/runtime/routes/conversation-query-routes.ts +53 -11
- package/src/runtime/routes/document-comments-routes.ts +8 -2
- package/src/runtime/routes/guardian-action-routes.ts +7 -0
- package/src/runtime/routes/heartbeat-routes.ts +41 -53
- package/src/runtime/routes/host-app-control-routes.ts +10 -36
- package/src/runtime/routes/host-bash-routes.ts +11 -45
- package/src/runtime/routes/host-browser-routes.ts +24 -0
- package/src/runtime/routes/host-cu-routes.ts +11 -45
- package/src/runtime/routes/host-file-routes.ts +11 -44
- package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
- package/src/runtime/routes/host-transfer-routes.ts +33 -25
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/integrations/a2a.ts +12 -3
- package/src/runtime/routes/integrations/vercel.ts +13 -3
- package/src/runtime/routes/platform-routes.ts +121 -24
- package/src/runtime/routes/schedule-routes.ts +15 -1
- package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
- package/src/schedule/schedule-timezone.ts +25 -0
- package/src/schedule/scheduler.ts +19 -0
- package/src/schedule/worker.ts +119 -2
- package/src/skills/catalog-install.ts +40 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
- package/src/tools/browser/browser-execution.ts +117 -53
- package/src/tools/browser/browser-status-constants.ts +6 -6
- package/src/tools/capability-offer.test.ts +87 -0
- package/src/tools/capability-offer.ts +93 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/util/provider-error-patterns.ts +34 -0
- package/src/watcher/__tests__/engine.test.ts +5 -2
- package/src/watcher/engine.ts +7 -4
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
- package/src/runtime/routes/sight-frame-routes.ts +0 -136
|
@@ -14,6 +14,10 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { dirname, join, posix, resolve, sep } from "node:path";
|
|
15
15
|
import { gunzipSync } from "node:zlib";
|
|
16
16
|
|
|
17
|
+
import {
|
|
18
|
+
MALFORMED_USTAR_SIZE,
|
|
19
|
+
parseUstarSizeField,
|
|
20
|
+
} from "../archive/ustar-size.js";
|
|
17
21
|
import { getPlatformBaseUrl } from "../config/env.js";
|
|
18
22
|
import { loadSkillCatalog } from "../config/skills.js";
|
|
19
23
|
import { isBunVirtualPath } from "../util/bundled-asset.js";
|
|
@@ -29,6 +33,35 @@ import {
|
|
|
29
33
|
|
|
30
34
|
const log = getLogger("catalog-install");
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* `bun install` argv for a skill's declared dependencies. Lifecycle scripts
|
|
38
|
+
* are suppressed: skill archives (catalog tarballs, skills.sh, ClawHub) are
|
|
39
|
+
* unsigned, so a `preinstall`/`postinstall` hook would be arbitrary code
|
|
40
|
+
* execution at install time. Mirrors plugin install in
|
|
41
|
+
* `cli/lib/install-plugin-dependencies.ts`.
|
|
42
|
+
*/
|
|
43
|
+
export const SKILL_DEPENDENCY_INSTALL_ARGS: readonly string[] = Object.freeze([
|
|
44
|
+
"install",
|
|
45
|
+
"--omit=dev",
|
|
46
|
+
"--ignore-scripts",
|
|
47
|
+
"--no-save",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export class SkillArchiveError extends Error {
|
|
51
|
+
constructor(message: string) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "SkillArchiveError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseTarSize(header: Buffer): number {
|
|
58
|
+
try {
|
|
59
|
+
return parseUstarSizeField(header);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new SkillArchiveError(MALFORMED_USTAR_SIZE);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
33
66
|
|
|
34
67
|
export interface CatalogSkill {
|
|
@@ -302,11 +335,13 @@ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
|
|
|
302
335
|
// File type (byte 156): '5' = directory, '0' or '\0' = regular file
|
|
303
336
|
const typeFlag = header[156];
|
|
304
337
|
|
|
305
|
-
|
|
306
|
-
const sizeStr = header.subarray(124, 136).toString("utf-8").trim();
|
|
307
|
-
const size = parseInt(sizeStr, 8) || 0;
|
|
338
|
+
const size = parseTarSize(header);
|
|
308
339
|
|
|
309
340
|
offset += 512; // past header
|
|
341
|
+
const paddedSize = Math.ceil(size / 512) * 512;
|
|
342
|
+
if (offset + paddedSize > tarBuffer.length) {
|
|
343
|
+
throw new SkillArchiveError("tar entry size exceeds archive length");
|
|
344
|
+
}
|
|
310
345
|
|
|
311
346
|
// Skip directories and empty names
|
|
312
347
|
if (name && typeFlag !== 53 /* '5' */) {
|
|
@@ -325,7 +360,7 @@ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
|
|
|
325
360
|
}
|
|
326
361
|
|
|
327
362
|
// Skip to next header (data padded to 512 bytes)
|
|
328
|
-
offset +=
|
|
363
|
+
offset += paddedSize;
|
|
329
364
|
}
|
|
330
365
|
return foundSkillMd;
|
|
331
366
|
}
|
|
@@ -419,7 +454,7 @@ export async function installSkillDependenciesIfPresent(
|
|
|
419
454
|
const env = { ...process.env };
|
|
420
455
|
addToPathEnv(env, [join(homedir(), ".bun", "bin")]);
|
|
421
456
|
await new Promise<void>((resolve, reject) => {
|
|
422
|
-
const child = spawn("bun", [
|
|
457
|
+
const child = spawn("bun", [...SKILL_DEPENDENCY_INSTALL_ARGS], {
|
|
423
458
|
cwd: skillDir,
|
|
424
459
|
stdio: "inherit",
|
|
425
460
|
env,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
BROWSER_STATUS_INPUT_FIELD,
|
|
7
7
|
BROWSER_STATUS_MODE,
|
|
8
8
|
CHROME_EXTENSION_INSTALL_HINT,
|
|
9
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
9
10
|
} from "../browser-status-constants.js";
|
|
10
11
|
import { CdpError } from "../cdp-client/errors.js";
|
|
11
12
|
|
|
@@ -217,6 +218,7 @@ describe("executeBrowserStatus", () => {
|
|
|
217
218
|
expect(extension.details.transport).toBe("extension-ws");
|
|
218
219
|
expect(extension.userActions).toEqual([
|
|
219
220
|
CHROME_EXTENSION_INSTALL_HINT,
|
|
221
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
220
222
|
"Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
|
|
221
223
|
]);
|
|
222
224
|
});
|
|
@@ -244,6 +246,7 @@ describe("executeBrowserStatus", () => {
|
|
|
244
246
|
expect(extension.summary).toContain("probe failed");
|
|
245
247
|
expect(extension.userActions).toEqual([
|
|
246
248
|
CHROME_EXTENSION_INSTALL_HINT,
|
|
249
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
247
250
|
"Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
|
|
248
251
|
]);
|
|
249
252
|
});
|
|
@@ -6,6 +6,11 @@ import { wrapUntrustedContent } from "../../security/untrusted-content.js";
|
|
|
6
6
|
import { getLogger } from "../../util/logger.js";
|
|
7
7
|
import { truncate } from "../../util/truncate.js";
|
|
8
8
|
import { safeStringSlice } from "../../util/unicode.js";
|
|
9
|
+
import {
|
|
10
|
+
appendLoggedInBrowserOffer,
|
|
11
|
+
formatLoggedInBrowserOffer,
|
|
12
|
+
shouldOfferLoggedInBrowser,
|
|
13
|
+
} from "../capability-offer.js";
|
|
9
14
|
import { credentialBroker } from "../credentials/broker.js";
|
|
10
15
|
import { BROWSER_FILL_CAPABILITY } from "../credentials/tool-policy.js";
|
|
11
16
|
import {
|
|
@@ -40,7 +45,7 @@ import {
|
|
|
40
45
|
type BrowserStatusMode,
|
|
41
46
|
CDP_INSPECT_STATUS_DISCOVERY_CODE,
|
|
42
47
|
CHROME_EXTENSION_INSTALL_HINT,
|
|
43
|
-
|
|
48
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
44
49
|
} from "./browser-status-constants.js";
|
|
45
50
|
import {
|
|
46
51
|
formatAxSnapshot,
|
|
@@ -298,10 +303,9 @@ const REMEDIATION_HINTS: Record<string, string[]> = {
|
|
|
298
303
|
// Extension backend
|
|
299
304
|
"extension:transport_error": [
|
|
300
305
|
CHROME_EXTENSION_INSTALL_HINT,
|
|
301
|
-
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"Try reconnecting the extension or restarting the desktop client.",
|
|
306
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
307
|
+
"Make sure Chrome is open with the Vellum extension enabled, or that the desktop app is running.",
|
|
308
|
+
"In the extension popup, check that the status shows connected.",
|
|
305
309
|
],
|
|
306
310
|
// cdp-inspect backend — discovery-level failures
|
|
307
311
|
"cdp-inspect:unreachable": [
|
|
@@ -334,15 +338,16 @@ const REMEDIATION_HINTS: Record<string, string[]> = {
|
|
|
334
338
|
],
|
|
335
339
|
// Host-bridge backend (desktop SSE bridge → user's Chrome debug port)
|
|
336
340
|
"host-bridge:unreachable": [
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
+
CHROME_EXTENSION_INSTALL_HINT,
|
|
342
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
343
|
+
"The Chrome extension is the preferred path and avoids the debug-port requirement.",
|
|
344
|
+
"If using remote debugging instead, Chrome 146+ must have remote debugging enabled at chrome://inspect/#remote-debugging.",
|
|
341
345
|
],
|
|
342
346
|
"host-bridge:transport_error": [
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"
|
|
347
|
+
CHROME_EXTENSION_INSTALL_HINT,
|
|
348
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
349
|
+
"The desktop app could not reach Chrome's remote-debugging endpoint.",
|
|
350
|
+
"Install the Chrome extension, or enable remote debugging and confirm the desktop app is running.",
|
|
346
351
|
],
|
|
347
352
|
// Local/Playwright backend
|
|
348
353
|
"local:transport_error": [
|
|
@@ -357,13 +362,15 @@ const REMEDIATION_HINTS: Record<string, string[]> = {
|
|
|
357
362
|
* pinned-mode failure. Includes:
|
|
358
363
|
* - the requested mode
|
|
359
364
|
* - ordered attempted modes with exact failure reasons
|
|
360
|
-
* - a
|
|
365
|
+
* - a logged-in-browser offer (desktop app and Chrome extension)
|
|
366
|
+
* - setup details tailored by backend and failure code
|
|
361
367
|
*
|
|
362
368
|
* Exported for testing.
|
|
363
369
|
*/
|
|
364
370
|
export function formatModeSelectionFailure(
|
|
365
371
|
requestedMode: BrowserMode,
|
|
366
372
|
error: CdpError,
|
|
373
|
+
context?: Pick<ToolContext, "transportInterface" | "clientOs">,
|
|
367
374
|
): string {
|
|
368
375
|
const lines: string[] = [];
|
|
369
376
|
lines.push(`Error: Browser mode "${requestedMode}" failed.`);
|
|
@@ -388,10 +395,12 @@ export function formatModeSelectionFailure(
|
|
|
388
395
|
lines.push("");
|
|
389
396
|
}
|
|
390
397
|
|
|
391
|
-
|
|
398
|
+
lines.push(formatLoggedInBrowserOffer(context));
|
|
399
|
+
lines.push("");
|
|
400
|
+
|
|
392
401
|
const hints = collectRemediationHints(diagnostics, error);
|
|
393
402
|
if (hints.length > 0) {
|
|
394
|
-
lines.push("
|
|
403
|
+
lines.push("Setup details:");
|
|
395
404
|
for (const hint of hints) {
|
|
396
405
|
lines.push(` - ${hint}`);
|
|
397
406
|
}
|
|
@@ -568,7 +577,7 @@ async function acquireCdpClientWithMode(
|
|
|
568
577
|
if (retryErr instanceof CdpError) {
|
|
569
578
|
return {
|
|
570
579
|
errorResult: {
|
|
571
|
-
content: formatModeSelectionFailure("auto", retryErr),
|
|
580
|
+
content: formatModeSelectionFailure("auto", retryErr, context),
|
|
572
581
|
isError: true,
|
|
573
582
|
},
|
|
574
583
|
};
|
|
@@ -579,7 +588,7 @@ async function acquireCdpClientWithMode(
|
|
|
579
588
|
if (err instanceof CdpError && browserMode !== "auto") {
|
|
580
589
|
return {
|
|
581
590
|
errorResult: {
|
|
582
|
-
content: formatModeSelectionFailure(browserMode, err),
|
|
591
|
+
content: formatModeSelectionFailure(browserMode, err, context),
|
|
583
592
|
isError: true,
|
|
584
593
|
},
|
|
585
594
|
};
|
|
@@ -658,6 +667,7 @@ function wrapWithKindMemo(
|
|
|
658
667
|
function formatCdpSendDiagnostics(
|
|
659
668
|
err: unknown,
|
|
660
669
|
browserMode: BrowserMode,
|
|
670
|
+
context?: Pick<ToolContext, "transportInterface" | "clientOs">,
|
|
661
671
|
): string | null {
|
|
662
672
|
if (
|
|
663
673
|
err instanceof CdpError &&
|
|
@@ -665,11 +675,60 @@ function formatCdpSendDiagnostics(
|
|
|
665
675
|
err.code === "transport_error" &&
|
|
666
676
|
err.attemptDiagnostics
|
|
667
677
|
) {
|
|
668
|
-
return formatModeSelectionFailure(browserMode, err);
|
|
678
|
+
return formatModeSelectionFailure(browserMode, err, context);
|
|
669
679
|
}
|
|
670
680
|
return null;
|
|
671
681
|
}
|
|
672
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Format a browser-tool catch as a tool-response error. Mode-selection
|
|
685
|
+
* failures already carry the logged-in-browser offer. Reachability and
|
|
686
|
+
* auth failures get the same offer appended so the model relays a product
|
|
687
|
+
* the user can turn on instead of asking for a screenshot.
|
|
688
|
+
*/
|
|
689
|
+
function formatBrowserToolFailure(
|
|
690
|
+
operation: string,
|
|
691
|
+
err: unknown,
|
|
692
|
+
browserMode: BrowserMode,
|
|
693
|
+
context: Pick<ToolContext, "transportInterface" | "clientOs">,
|
|
694
|
+
): string {
|
|
695
|
+
const diagnosticMessage = formatCdpSendDiagnostics(err, browserMode, context);
|
|
696
|
+
if (diagnosticMessage) {
|
|
697
|
+
return diagnosticMessage;
|
|
698
|
+
}
|
|
699
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
700
|
+
const base = `Error: ${operation} failed: ${msg}`;
|
|
701
|
+
if (shouldOfferLoggedInBrowser(msg)) {
|
|
702
|
+
return appendLoggedInBrowserOffer(base, context);
|
|
703
|
+
}
|
|
704
|
+
return base;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function appendLoginFormGuidance(
|
|
708
|
+
lines: string[],
|
|
709
|
+
context: ToolContext,
|
|
710
|
+
backendKind: string,
|
|
711
|
+
): void {
|
|
712
|
+
lines.push("Handle this by interacting with the login form:");
|
|
713
|
+
lines.push("1. Take a snapshot to find the sign-in form elements");
|
|
714
|
+
lines.push(
|
|
715
|
+
"2. Use credential fill to enter email/password from the credential vault",
|
|
716
|
+
);
|
|
717
|
+
lines.push(
|
|
718
|
+
"3. For email verification codes, use ui_show with a form to request the code mid-turn",
|
|
719
|
+
);
|
|
720
|
+
lines.push(
|
|
721
|
+
"4. Do NOT give up or suggest manual sign-in - handle the login flow yourself",
|
|
722
|
+
);
|
|
723
|
+
if (backendKind === "local") {
|
|
724
|
+
lines.push("");
|
|
725
|
+
lines.push(
|
|
726
|
+
"If this page needs SSO, a company VPN, or a login a fresh browser cannot complete:",
|
|
727
|
+
);
|
|
728
|
+
lines.push(formatLoggedInBrowserOffer(context));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
673
732
|
// ── Shared element resolution ────────────────────────────────────────
|
|
674
733
|
|
|
675
734
|
/**
|
|
@@ -1234,19 +1293,7 @@ export async function executeBrowserNavigate(
|
|
|
1234
1293
|
),
|
|
1235
1294
|
);
|
|
1236
1295
|
lines.push("");
|
|
1237
|
-
lines
|
|
1238
|
-
lines.push(
|
|
1239
|
-
"1. Take a snapshot to find the sign-in form elements",
|
|
1240
|
-
);
|
|
1241
|
-
lines.push(
|
|
1242
|
-
"2. Use credential fill to enter email/password from the credential vault",
|
|
1243
|
-
);
|
|
1244
|
-
lines.push(
|
|
1245
|
-
"3. For email verification codes, use ui_show with a form to request the code mid-turn",
|
|
1246
|
-
);
|
|
1247
|
-
lines.push(
|
|
1248
|
-
"4. Do NOT give up or suggest manual sign-in - handle the login flow yourself",
|
|
1249
|
-
);
|
|
1296
|
+
appendLoginFormGuidance(lines, context, cdp.kind);
|
|
1250
1297
|
}
|
|
1251
1298
|
} else {
|
|
1252
1299
|
lines.push("");
|
|
@@ -1256,6 +1303,10 @@ export async function executeBrowserNavigate(
|
|
|
1256
1303
|
lines.push(
|
|
1257
1304
|
"This challenge requires human verification. Surface this clearly: the page cannot be accessed until the verification is solved manually.",
|
|
1258
1305
|
);
|
|
1306
|
+
if (cdp.kind === "local") {
|
|
1307
|
+
lines.push("");
|
|
1308
|
+
lines.push(formatLoggedInBrowserOffer(context));
|
|
1309
|
+
}
|
|
1259
1310
|
}
|
|
1260
1311
|
} else {
|
|
1261
1312
|
// Login / 2FA / OAuth - the agent should handle these itself
|
|
@@ -1271,17 +1322,7 @@ export async function executeBrowserNavigate(
|
|
|
1271
1322
|
),
|
|
1272
1323
|
);
|
|
1273
1324
|
lines.push("");
|
|
1274
|
-
lines
|
|
1275
|
-
lines.push("1. Take a snapshot to find the sign-in form elements");
|
|
1276
|
-
lines.push(
|
|
1277
|
-
"2. Use credential fill to enter email/password from the credential vault",
|
|
1278
|
-
);
|
|
1279
|
-
lines.push(
|
|
1280
|
-
"3. For email verification codes, use ui_show with a form to request the code mid-turn",
|
|
1281
|
-
);
|
|
1282
|
-
lines.push(
|
|
1283
|
-
"4. Do NOT give up or suggest manual sign-in - handle the login flow yourself",
|
|
1284
|
-
);
|
|
1325
|
+
appendLoginFormGuidance(lines, context, cdp.kind);
|
|
1285
1326
|
}
|
|
1286
1327
|
}
|
|
1287
1328
|
} catch {
|
|
@@ -1312,14 +1353,16 @@ export async function executeBrowserNavigate(
|
|
|
1312
1353
|
};
|
|
1313
1354
|
}
|
|
1314
1355
|
|
|
1315
|
-
const diagnosticMessage = formatCdpSendDiagnostics(err, browserMode);
|
|
1316
|
-
if (diagnosticMessage) {
|
|
1317
|
-
return { content: diagnosticMessage, isError: true };
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1321
1356
|
log.error({ err, url: safeRequestedUrl }, "Navigation failed");
|
|
1322
|
-
return {
|
|
1357
|
+
return {
|
|
1358
|
+
content: formatBrowserToolFailure(
|
|
1359
|
+
"Navigation",
|
|
1360
|
+
err,
|
|
1361
|
+
browserMode,
|
|
1362
|
+
context,
|
|
1363
|
+
),
|
|
1364
|
+
isError: true,
|
|
1365
|
+
};
|
|
1323
1366
|
} finally {
|
|
1324
1367
|
cdp.dispose();
|
|
1325
1368
|
}
|
|
@@ -1375,7 +1418,11 @@ export async function executeBrowserSnapshot(
|
|
|
1375
1418
|
isError: false,
|
|
1376
1419
|
};
|
|
1377
1420
|
} catch (err) {
|
|
1378
|
-
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1421
|
+
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1422
|
+
err,
|
|
1423
|
+
browserMode,
|
|
1424
|
+
context,
|
|
1425
|
+
);
|
|
1379
1426
|
if (diagnosticMessage) {
|
|
1380
1427
|
return { content: diagnosticMessage, isError: true };
|
|
1381
1428
|
}
|
|
@@ -1434,7 +1481,11 @@ export async function executeBrowserScreenshot(
|
|
|
1434
1481
|
contentBlocks: [imageBlock],
|
|
1435
1482
|
};
|
|
1436
1483
|
} catch (err) {
|
|
1437
|
-
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1484
|
+
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1485
|
+
err,
|
|
1486
|
+
browserMode,
|
|
1487
|
+
context,
|
|
1488
|
+
);
|
|
1438
1489
|
if (diagnosticMessage) {
|
|
1439
1490
|
return { content: diagnosticMessage, isError: true };
|
|
1440
1491
|
}
|
|
@@ -1489,6 +1540,7 @@ export async function executeBrowserAttach(
|
|
|
1489
1540
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1490
1541
|
err,
|
|
1491
1542
|
acquired.browserMode,
|
|
1543
|
+
context,
|
|
1492
1544
|
);
|
|
1493
1545
|
if (diagnosticMessage) {
|
|
1494
1546
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -1535,6 +1587,7 @@ export async function executeBrowserDetach(
|
|
|
1535
1587
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1536
1588
|
err,
|
|
1537
1589
|
acquired.browserMode,
|
|
1590
|
+
context,
|
|
1538
1591
|
);
|
|
1539
1592
|
if (diagnosticMessage) {
|
|
1540
1593
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -1611,6 +1664,7 @@ export async function executeBrowserClose(
|
|
|
1611
1664
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1612
1665
|
err,
|
|
1613
1666
|
acquired.browserMode,
|
|
1667
|
+
context,
|
|
1614
1668
|
);
|
|
1615
1669
|
if (diagnosticMessage) {
|
|
1616
1670
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -1669,6 +1723,7 @@ export async function executeBrowserClick(
|
|
|
1669
1723
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1670
1724
|
err,
|
|
1671
1725
|
acquired.browserMode,
|
|
1726
|
+
context,
|
|
1672
1727
|
);
|
|
1673
1728
|
if (diagnosticMessage) {
|
|
1674
1729
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -1799,6 +1854,7 @@ export async function executeBrowserType(
|
|
|
1799
1854
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1800
1855
|
err,
|
|
1801
1856
|
acquired.browserMode,
|
|
1857
|
+
context,
|
|
1802
1858
|
);
|
|
1803
1859
|
if (diagnosticMessage) {
|
|
1804
1860
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -1874,6 +1930,7 @@ export async function executeBrowserPressKey(
|
|
|
1874
1930
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1875
1931
|
err,
|
|
1876
1932
|
acquired.browserMode,
|
|
1933
|
+
context,
|
|
1877
1934
|
);
|
|
1878
1935
|
if (diagnosticMessage) {
|
|
1879
1936
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -1949,6 +2006,7 @@ export async function executeBrowserScroll(
|
|
|
1949
2006
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
1950
2007
|
err,
|
|
1951
2008
|
acquired.browserMode,
|
|
2009
|
+
context,
|
|
1952
2010
|
);
|
|
1953
2011
|
if (diagnosticMessage) {
|
|
1954
2012
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -2085,6 +2143,7 @@ export async function executeBrowserSelectOption(
|
|
|
2085
2143
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
2086
2144
|
err,
|
|
2087
2145
|
acquired.browserMode,
|
|
2146
|
+
context,
|
|
2088
2147
|
);
|
|
2089
2148
|
if (diagnosticMessage) {
|
|
2090
2149
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -2140,6 +2199,7 @@ export async function executeBrowserHover(
|
|
|
2140
2199
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
2141
2200
|
err,
|
|
2142
2201
|
acquired.browserMode,
|
|
2202
|
+
context,
|
|
2143
2203
|
);
|
|
2144
2204
|
if (diagnosticMessage) {
|
|
2145
2205
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -2235,6 +2295,7 @@ export async function executeBrowserWaitFor(
|
|
|
2235
2295
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
2236
2296
|
err,
|
|
2237
2297
|
acquired.browserMode,
|
|
2298
|
+
context,
|
|
2238
2299
|
);
|
|
2239
2300
|
if (diagnosticMessage) {
|
|
2240
2301
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -2320,6 +2381,7 @@ export async function executeBrowserExtract(
|
|
|
2320
2381
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
2321
2382
|
err,
|
|
2322
2383
|
acquired.browserMode,
|
|
2384
|
+
context,
|
|
2323
2385
|
);
|
|
2324
2386
|
if (diagnosticMessage) {
|
|
2325
2387
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -2455,6 +2517,7 @@ export async function executeBrowserFillCredential(
|
|
|
2455
2517
|
const diagnosticMessage = formatCdpSendDiagnostics(
|
|
2456
2518
|
err,
|
|
2457
2519
|
acquired.browserMode,
|
|
2520
|
+
context,
|
|
2458
2521
|
);
|
|
2459
2522
|
if (diagnosticMessage) {
|
|
2460
2523
|
return { content: diagnosticMessage, isError: true };
|
|
@@ -2490,6 +2553,7 @@ function modeTradeoffs(mode: StatusCheckMode): string[] {
|
|
|
2490
2553
|
function extensionConnectionActions(): string[] {
|
|
2491
2554
|
return [
|
|
2492
2555
|
CHROME_EXTENSION_INSTALL_HINT,
|
|
2556
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
2493
2557
|
"Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
|
|
2494
2558
|
];
|
|
2495
2559
|
}
|
|
@@ -2586,7 +2650,7 @@ async function probePinnedBrowserMode(
|
|
|
2586
2650
|
return {
|
|
2587
2651
|
ok: false,
|
|
2588
2652
|
error: err,
|
|
2589
|
-
diagnostic: formatModeSelectionFailure(mode, err),
|
|
2653
|
+
diagnostic: formatModeSelectionFailure(mode, err, context),
|
|
2590
2654
|
};
|
|
2591
2655
|
}
|
|
2592
2656
|
const wrapped = new CdpError(
|
|
@@ -2597,7 +2661,7 @@ async function probePinnedBrowserMode(
|
|
|
2597
2661
|
return {
|
|
2598
2662
|
ok: false,
|
|
2599
2663
|
error: wrapped,
|
|
2600
|
-
diagnostic: formatModeSelectionFailure(mode, wrapped),
|
|
2664
|
+
diagnostic: formatModeSelectionFailure(mode, wrapped, context),
|
|
2601
2665
|
};
|
|
2602
2666
|
} finally {
|
|
2603
2667
|
cdp?.dispose();
|
|
@@ -20,12 +20,12 @@ export const BROWSER_STATUS_INPUT_FIELD = {
|
|
|
20
20
|
CHECK_LOCAL_LAUNCH: "check_local_launch",
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
export {
|
|
24
|
+
CHROME_EXTENSION_INSTALL_HINT,
|
|
25
|
+
CHROME_WEB_STORE_INSTALL_URL,
|
|
26
|
+
DESKTOP_APP_DOWNLOAD_URL,
|
|
27
|
+
DESKTOP_APP_INSTALL_HINT,
|
|
28
|
+
} from "../capability-offer.js";
|
|
29
29
|
|
|
30
30
|
export const CDP_INSPECT_STATUS_DISCOVERY_CODE = {
|
|
31
31
|
NO_TARGETS: DEVTOOLS_DISCOVERY_CODE.NO_TARGETS,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
appendLoggedInBrowserOffer,
|
|
5
|
+
CHROME_WEB_STORE_INSTALL_URL,
|
|
6
|
+
DESKTOP_APP_DOWNLOAD_URL,
|
|
7
|
+
formatLoggedInBrowserOffer,
|
|
8
|
+
isPhoneSurface,
|
|
9
|
+
shouldOfferLoggedInBrowser,
|
|
10
|
+
} from "./capability-offer.js";
|
|
11
|
+
|
|
12
|
+
describe("isPhoneSurface", () => {
|
|
13
|
+
test("is true for iOS transport and mobile client OS", () => {
|
|
14
|
+
expect(isPhoneSurface({ transportInterface: "ios" })).toBe(true);
|
|
15
|
+
expect(isPhoneSurface({ transportInterface: "phone" })).toBe(true);
|
|
16
|
+
expect(isPhoneSurface({ clientOs: "ios" })).toBe(true);
|
|
17
|
+
expect(isPhoneSurface({ clientOs: "android" })).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("is false for desktop and web surfaces", () => {
|
|
21
|
+
expect(isPhoneSurface({ transportInterface: "web" })).toBe(false);
|
|
22
|
+
expect(isPhoneSurface({ transportInterface: "macos" })).toBe(false);
|
|
23
|
+
expect(isPhoneSurface({ clientOs: "macos" })).toBe(false);
|
|
24
|
+
expect(isPhoneSurface({})).toBe(false);
|
|
25
|
+
expect(isPhoneSurface()).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("shouldOfferLoggedInBrowser", () => {
|
|
30
|
+
test("matches timeouts, unreachable hosts, and auth walls", () => {
|
|
31
|
+
expect(
|
|
32
|
+
shouldOfferLoggedInBrowser(
|
|
33
|
+
"Navigation to https://intranet.example.com timed out after 15000ms",
|
|
34
|
+
),
|
|
35
|
+
).toBe(true);
|
|
36
|
+
expect(shouldOfferLoggedInBrowser("net::ERR_CONNECTION_TIMED_OUT")).toBe(
|
|
37
|
+
true,
|
|
38
|
+
);
|
|
39
|
+
expect(shouldOfferLoggedInBrowser("ENOTFOUND intranet.example.com")).toBe(
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
expect(shouldOfferLoggedInBrowser("host unreachable")).toBe(true);
|
|
43
|
+
expect(shouldOfferLoggedInBrowser("login required")).toBe(true);
|
|
44
|
+
expect(shouldOfferLoggedInBrowser("HTTP 403 Forbidden")).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("does not match cancelled or invalid-input failures", () => {
|
|
48
|
+
expect(shouldOfferLoggedInBrowser("operation was cancelled")).toBe(false);
|
|
49
|
+
expect(shouldOfferLoggedInBrowser('Invalid browser_mode "bogus"')).toBe(
|
|
50
|
+
false,
|
|
51
|
+
);
|
|
52
|
+
expect(shouldOfferLoggedInBrowser("url must use http or https")).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("formatLoggedInBrowserOffer", () => {
|
|
57
|
+
test("offers both install links and prefers them over a screenshot", () => {
|
|
58
|
+
const offer = formatLoggedInBrowserOffer({ transportInterface: "web" });
|
|
59
|
+
expect(offer).toContain(DESKTOP_APP_DOWNLOAD_URL);
|
|
60
|
+
expect(offer).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
61
|
+
expect(offer).toContain("Offer those first");
|
|
62
|
+
expect(offer).toContain("screenshot");
|
|
63
|
+
expect(offer).not.toContain("daemon");
|
|
64
|
+
expect(offer).not.toContain("browser panel");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("is honest on a phone: no in-app browser, install on a computer", () => {
|
|
68
|
+
const offer = formatLoggedInBrowserOffer({
|
|
69
|
+
transportInterface: "ios",
|
|
70
|
+
clientOs: "ios",
|
|
71
|
+
});
|
|
72
|
+
expect(offer).toContain("no in-app browser");
|
|
73
|
+
expect(offer).toContain("Do not describe a browser panel");
|
|
74
|
+
expect(offer).toContain(DESKTOP_APP_DOWNLOAD_URL);
|
|
75
|
+
expect(offer).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("appendLoggedInBrowserOffer", () => {
|
|
80
|
+
test("appends the offer once", () => {
|
|
81
|
+
const once = appendLoggedInBrowserOffer("Error: Navigation failed: timeout");
|
|
82
|
+
expect(once).toContain("Error: Navigation failed: timeout");
|
|
83
|
+
expect(once).toContain(DESKTOP_APP_DOWNLOAD_URL);
|
|
84
|
+
const twice = appendLoggedInBrowserOffer(once);
|
|
85
|
+
expect(twice).toBe(once);
|
|
86
|
+
});
|
|
87
|
+
});
|