@vellumai/assistant 0.6.2 → 0.6.3

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.
Files changed (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -9,6 +9,55 @@ mock.module("../util/logger.js", () => ({
9
9
  }),
10
10
  }));
11
11
 
12
+ /**
13
+ * Fake CDP session used by the screenshot/extract/wait_for tests in
14
+ * this file. The tests configure `sendHandler` before invoking a
15
+ * tool; each `session.send(method, params)` call is recorded in
16
+ * `sendCalls` and routed to the handler. The handler returns either
17
+ * a CDP response object (e.g. `{ result: { value: ... } }` for
18
+ * `Runtime.evaluate`, or `{ data }` for `Page.captureScreenshot`) or
19
+ * an `Error` to simulate a CDP failure.
20
+ *
21
+ * The fake session is exposed via `mockPage.context().newCDPSession(page)`
22
+ * which is what the real `LocalCdpClient` calls internally. Going
23
+ * through the real `LocalCdpClient` (instead of mocking the factory
24
+ * or the cdp-client submodules) avoids polluting the global module
25
+ * cache that the `factory.test.ts` and LocalCdpClient/
26
+ * ExtensionCdpClient unit tests rely on.
27
+ */
28
+ interface SendCall {
29
+ method: string;
30
+ params: Record<string, unknown> | undefined;
31
+ }
32
+
33
+ let sendCalls: SendCall[];
34
+ let sendHandler: (
35
+ method: string,
36
+ params: Record<string, unknown> | undefined,
37
+ ) => unknown;
38
+
39
+ function resetCdpMock() {
40
+ sendCalls = [];
41
+ sendHandler = () => ({});
42
+ }
43
+
44
+ const fakeCdpSession = {
45
+ send: async (method: string, params?: Record<string, unknown>) => {
46
+ sendCalls.push({ method, params });
47
+ const value = sendHandler(method, params);
48
+ if (value instanceof Error) throw value;
49
+ return value;
50
+ },
51
+ // Provided so `LocalCdpClient.dispose()` can call `session.detach()`
52
+ // without throwing. The tests don't assert on detach calls.
53
+ detach: async () => {},
54
+ };
55
+
56
+ // The mock page is served by `browserManager.getOrCreateSessionPage`
57
+ // and is consumed indirectly: LocalCdpClient calls
58
+ // `page.context().newCDPSession(page)` to obtain a CDP session and
59
+ // then dispatches raw CDP methods against it. The page's
60
+ // `context().newCDPSession` is wired to return `fakeCdpSession` above.
12
61
  let mockPage: {
13
62
  click: ReturnType<typeof mock>;
14
63
  fill: ReturnType<typeof mock>;
@@ -22,25 +71,17 @@ let mockPage: {
22
71
  waitForSelector: ReturnType<typeof mock>;
23
72
  waitForFunction: ReturnType<typeof mock>;
24
73
  keyboard: { press: ReturnType<typeof mock> };
74
+ context: () => {
75
+ newCDPSession: (page: unknown) => Promise<typeof fakeCdpSession>;
76
+ };
25
77
  };
26
78
 
27
- let snapshotMaps: Map<string, Map<string, string>>;
28
-
29
79
  mock.module("../tools/browser/browser-manager.js", () => {
30
- snapshotMaps = new Map();
31
80
  return {
32
81
  browserManager: {
33
82
  getOrCreateSessionPage: async () => mockPage,
34
83
  closeSessionPage: async () => {},
35
84
  closeAllPages: async () => {},
36
- storeSnapshotMap: (conversationId: string, map: Map<string, string>) => {
37
- snapshotMaps.set(conversationId, map);
38
- },
39
- resolveSnapshotSelector: (conversationId: string, elementId: string) => {
40
- const map = snapshotMaps.get(conversationId);
41
- if (!map) return null;
42
- return map.get(elementId) ?? null;
43
- },
44
85
  },
45
86
  };
46
87
  });
@@ -55,8 +96,9 @@ mock.module("../tools/network/url-safety.js", () => ({
55
96
 
56
97
  import {
57
98
  executeBrowserExtract,
58
- executeBrowserPressKey,
99
+ executeBrowserScreenshot,
59
100
  executeBrowserWaitFor,
101
+ EXTRACT_LINKS_EXPRESSION,
60
102
  } from "../tools/browser/browser-execution.js";
61
103
  import type { ToolContext } from "../tools/types.js";
62
104
 
@@ -83,72 +125,76 @@ function resetMockPage() {
83
125
  waitForSelector: mock(async () => null),
84
126
  waitForFunction: mock(async () => null),
85
127
  keyboard: { press: mock(async () => {}) },
128
+ // `LocalCdpClient.ensureSession()` calls `page.context().newCDPSession(
129
+ // page)` to create a Playwright CDPSession. For these tests we
130
+ // return the in-file `fakeCdpSession` which records every send()
131
+ // into `sendCalls` and lets each test set `sendHandler` to shape
132
+ // the responses.
133
+ context: () => ({
134
+ newCDPSession: async (_page: unknown) => fakeCdpSession,
135
+ }),
86
136
  };
87
137
  }
88
138
 
89
- // ── browser_press_key ────────────────────────────────────────────────
139
+ // executeBrowserPressKey tests live in
140
+ // `headless-browser-interactions.test.ts` alongside the other
141
+ // CDP-migrated interaction tools.
90
142
 
91
- describe("executeBrowserPressKey", () => {
143
+ // ── browser_screenshot ───────────────────────────────────────────────
144
+
145
+ describe("executeBrowserScreenshot", () => {
92
146
  beforeEach(() => {
93
147
  resetMockPage();
94
- snapshotMaps.clear();
148
+ resetCdpMock();
95
149
  });
96
150
 
97
- test("presses key on focused element (no target)", async () => {
98
- const result = await executeBrowserPressKey({ key: "Enter" }, ctx);
99
- expect(result.isError).toBe(false);
100
- expect(result.content).toContain('Pressed "Enter"');
101
- expect(mockPage.keyboard.press).toHaveBeenCalledWith("Enter");
102
- });
151
+ test("captures viewport screenshot via Page.captureScreenshot", async () => {
152
+ // Base64 for "abc" = "YWJj"
153
+ sendHandler = () => ({ data: "YWJj" });
154
+
155
+ const result = await executeBrowserScreenshot({}, ctx);
103
156
 
104
- test("presses key on targeted element via element_id", async () => {
105
- snapshotMaps.set(
106
- "test-conversation",
107
- new Map([["e1", '[data-vellum-eid="e1"]']]),
108
- );
109
- const result = await executeBrowserPressKey(
110
- { key: "Tab", element_id: "e1" },
111
- ctx,
112
- );
113
157
  expect(result.isError).toBe(false);
114
- expect(result.content).toContain('Pressed "Tab" on element');
115
- expect(mockPage.press).toHaveBeenCalledWith(
116
- '[data-vellum-eid="e1"]',
117
- "Tab",
118
- );
158
+ expect(result.content).toContain("Screenshot captured");
159
+ expect(result.content).toContain("viewport");
160
+ expect(result.contentBlocks).toHaveLength(1);
161
+ const block = result.contentBlocks![0]!;
162
+
163
+ const source = (block as any).source;
164
+ expect(source.media_type).toBe("image/jpeg");
165
+ expect(source.data).toBe("YWJj");
166
+ // Page.captureScreenshot was called with jpeg quality 80
167
+ expect(sendCalls).toHaveLength(1);
168
+ expect(sendCalls[0]!.method).toBe("Page.captureScreenshot");
169
+ expect(sendCalls[0]!.params).toEqual({
170
+ format: "jpeg",
171
+ quality: 80,
172
+ captureBeyondViewport: false,
173
+ });
119
174
  });
120
175
 
121
- test("presses key on targeted element via selector", async () => {
122
- const result = await executeBrowserPressKey(
123
- { key: "Escape", selector: "#modal" },
124
- ctx,
125
- );
176
+ test("captures full-page screenshot when full_page is true", async () => {
177
+ sendHandler = () => ({ data: "YWJj" });
178
+
179
+ const result = await executeBrowserScreenshot({ full_page: true }, ctx);
180
+
126
181
  expect(result.isError).toBe(false);
127
- expect(mockPage.press).toHaveBeenCalledWith("#modal", "Escape");
182
+ expect(result.content).toContain("full page");
183
+ expect(sendCalls[0]!.params).toEqual({
184
+ format: "jpeg",
185
+ quality: 80,
186
+ captureBeyondViewport: true,
187
+ });
128
188
  });
129
189
 
130
- test("errors when key is missing", async () => {
131
- const result = await executeBrowserPressKey({}, ctx);
132
- expect(result.isError).toBe(true);
133
- expect(result.content).toContain("key is required");
134
- });
190
+ test("surfaces CDP failure as an error result", async () => {
191
+ sendHandler = () => new Error("CDP crashed");
135
192
 
136
- test("errors when element_id not found", async () => {
137
- const result = await executeBrowserPressKey(
138
- { key: "Enter", element_id: "e99" },
139
- ctx,
140
- );
141
- expect(result.isError).toBe(true);
142
- expect(result.content).toContain('element_id "e99" not found');
143
- });
193
+ const result = await executeBrowserScreenshot({}, ctx);
144
194
 
145
- test("handles press error", async () => {
146
- mockPage.keyboard.press = mock(async () => {
147
- throw new Error("key not recognized");
148
- });
149
- const result = await executeBrowserPressKey({ key: "InvalidKey" }, ctx);
150
195
  expect(result.isError).toBe(true);
151
- expect(result.content).toContain("Press key failed");
196
+ expect(result.content).toContain("Screenshot failed");
197
+ expect(result.content).toContain("CDP crashed");
152
198
  });
153
199
  });
154
200
 
@@ -157,28 +203,67 @@ describe("executeBrowserPressKey", () => {
157
203
  describe("executeBrowserWaitFor", () => {
158
204
  beforeEach(() => {
159
205
  resetMockPage();
206
+ resetCdpMock();
160
207
  });
161
208
 
162
- test("waits for selector", async () => {
209
+ test("waits for selector (CDP polling)", async () => {
210
+ // waitForSelector polls via Runtime.evaluate until the element
211
+ // exists, then calls querySelectorBackendNodeId (which triggers
212
+ // DOM.getDocument / DOM.querySelector / DOM.describeNode).
213
+ sendHandler = (method, _params) => {
214
+ if (method === "Runtime.evaluate") {
215
+ return { result: { value: true } };
216
+ }
217
+ if (method === "DOM.getDocument") return { root: { nodeId: 1 } };
218
+ if (method === "DOM.querySelector") return { nodeId: 42 };
219
+ if (method === "DOM.describeNode") {
220
+ return { node: { backendNodeId: 100 } };
221
+ }
222
+ return {};
223
+ };
224
+
163
225
  const result = await executeBrowserWaitFor({ selector: "#loaded" }, ctx);
226
+
164
227
  expect(result.isError).toBe(false);
165
228
  expect(result.content).toContain('Element matching "#loaded" appeared');
166
- expect(mockPage.waitForSelector).toHaveBeenCalledWith("#loaded", {
167
- timeout: 30_000,
168
- });
229
+ // First call is Runtime.evaluate checking existence
230
+ const evaluateCall = sendCalls.find((c) => c.method === "Runtime.evaluate");
231
+ expect(evaluateCall).toBeDefined();
232
+ expect((evaluateCall!.params as { expression: string }).expression).toBe(
233
+ 'document.querySelector("#loaded") !== null',
234
+ );
169
235
  });
170
236
 
171
- test("waits for text", async () => {
237
+ test("waits for text (CDP polling)", async () => {
238
+ sendHandler = (method) => {
239
+ if (method === "Runtime.evaluate") {
240
+ return { result: { value: true } };
241
+ }
242
+ return {};
243
+ };
244
+
172
245
  const result = await executeBrowserWaitFor({ text: "Success" }, ctx);
246
+
173
247
  expect(result.isError).toBe(false);
174
248
  expect(result.content).toContain('Text "Success" appeared');
175
- expect(mockPage.waitForFunction).toHaveBeenCalled();
249
+ const evaluateCall = sendCalls.find((c) => c.method === "Runtime.evaluate");
250
+ expect(evaluateCall).toBeDefined();
251
+ expect(
252
+ (evaluateCall!.params as { expression: string }).expression,
253
+ ).toContain('"Success"');
254
+ expect(
255
+ (evaluateCall!.params as { expression: string }).expression,
256
+ ).toContain(".includes(");
176
257
  });
177
258
 
178
- test("waits for duration", async () => {
259
+ test("waits for duration (no CDP client acquired)", async () => {
179
260
  const result = await executeBrowserWaitFor({ duration: 10 }, ctx);
261
+
180
262
  expect(result.isError).toBe(false);
181
263
  expect(result.content).toContain("Waited 10ms");
264
+ // Duration mode is transport-agnostic and must not allocate a
265
+ // CdpClient — so neither send nor dispose should have fired.
266
+ expect(sendCalls).toHaveLength(0);
182
267
  });
183
268
 
184
269
  test("errors when no mode specified", async () => {
@@ -187,6 +272,8 @@ describe("executeBrowserWaitFor", () => {
187
272
  expect(result.content).toContain(
188
273
  "Exactly one of selector, text, or duration",
189
274
  );
275
+ // Validation rejects before any CDP work is attempted.
276
+ expect(sendCalls).toHaveLength(0);
190
277
  });
191
278
 
192
279
  test("errors when multiple modes specified", async () => {
@@ -196,17 +283,7 @@ describe("executeBrowserWaitFor", () => {
196
283
  );
197
284
  expect(result.isError).toBe(true);
198
285
  expect(result.content).toContain("exactly one");
199
- });
200
-
201
- test("respects custom timeout", async () => {
202
- const result = await executeBrowserWaitFor(
203
- { selector: "#el", timeout: 5000 },
204
- ctx,
205
- );
206
- expect(result.isError).toBe(false);
207
- expect(mockPage.waitForSelector).toHaveBeenCalledWith("#el", {
208
- timeout: 5000,
209
- });
286
+ expect(sendCalls).toHaveLength(0);
210
287
  });
211
288
 
212
289
  test("caps duration at MAX_WAIT_MS", async () => {
@@ -217,14 +294,16 @@ describe("executeBrowserWaitFor", () => {
217
294
  expect(result.content).toContain("Waited 50ms");
218
295
  });
219
296
 
220
- test("handles wait error (timeout)", async () => {
221
- mockPage.waitForSelector = mock(async () => {
222
- throw new Error("Timeout 30000ms exceeded");
223
- });
224
- const result = await executeBrowserWaitFor({ selector: "#missing" }, ctx);
297
+ test("surfaces CDP transport failure as a wait error", async () => {
298
+ sendHandler = () => new Error("CDP transport failed");
299
+
300
+ const result = await executeBrowserWaitFor(
301
+ { selector: "#missing", timeout: 100 },
302
+ ctx,
303
+ );
304
+
225
305
  expect(result.isError).toBe(true);
226
306
  expect(result.content).toContain("Wait failed");
227
- expect(result.content).toContain("Timeout");
228
307
  });
229
308
  });
230
309
 
@@ -233,60 +312,144 @@ describe("executeBrowserWaitFor", () => {
233
312
  describe("executeBrowserExtract", () => {
234
313
  beforeEach(() => {
235
314
  resetMockPage();
315
+ resetCdpMock();
236
316
  });
237
317
 
238
- test("extracts page text content", async () => {
239
- mockPage.evaluate = mock(async () => "Hello World");
318
+ test("extracts page text content via CDP", async () => {
319
+ sendHandler = (method, params) => {
320
+ if (method !== "Runtime.evaluate") return {};
321
+ const expression = (params as { expression: string }).expression;
322
+ if (expression === "document.location.href") {
323
+ return { result: { value: "https://example.com/" } };
324
+ }
325
+ if (expression === "document.title") {
326
+ return { result: { value: "Test Page" } };
327
+ }
328
+ if (expression === "document.body?.innerText ?? ''") {
329
+ return { result: { value: "Hello World" } };
330
+ }
331
+ return { result: { value: null } };
332
+ };
333
+
240
334
  const result = await executeBrowserExtract({}, ctx);
335
+
241
336
  expect(result.isError).toBe(false);
242
337
  expect(result.content).toContain("URL: https://example.com/");
243
338
  expect(result.content).toContain("Title: Test Page");
244
339
  expect(result.content).toContain("Hello World");
340
+ // URL, title, body innerText = 3 Runtime.evaluate calls
341
+ const evaluateCalls = sendCalls.filter(
342
+ (c) => c.method === "Runtime.evaluate",
343
+ );
344
+ expect(evaluateCalls).toHaveLength(3);
245
345
  });
246
346
 
247
347
  test("shows (empty page) for empty content", async () => {
248
- mockPage.evaluate = mock(async () => "");
348
+ sendHandler = (method, params) => {
349
+ if (method !== "Runtime.evaluate") return {};
350
+ const expression = (params as { expression: string }).expression;
351
+ if (expression === "document.location.href") {
352
+ return { result: { value: "https://example.com/" } };
353
+ }
354
+ if (expression === "document.title") {
355
+ return { result: { value: "Test Page" } };
356
+ }
357
+ return { result: { value: "" } };
358
+ };
359
+
249
360
  const result = await executeBrowserExtract({}, ctx);
250
361
  expect(result.content).toContain("(empty page)");
251
362
  });
252
363
 
253
364
  test("truncates long content", async () => {
254
365
  const longText = "x".repeat(60_000);
255
- mockPage.evaluate = mock(async () => longText);
366
+ sendHandler = (method, params) => {
367
+ if (method !== "Runtime.evaluate") return {};
368
+ const expression = (params as { expression: string }).expression;
369
+ if (expression === "document.location.href") {
370
+ return { result: { value: "https://example.com/" } };
371
+ }
372
+ if (expression === "document.title") {
373
+ return { result: { value: "Test Page" } };
374
+ }
375
+ return { result: { value: longText } };
376
+ };
377
+
256
378
  const result = await executeBrowserExtract({}, ctx);
257
379
  expect(result.content).toContain("... (truncated)");
258
380
  // Content should be capped
259
381
  expect(result.content.length).toBeLessThan(60_000);
260
382
  });
261
383
 
262
- test("includes links when requested", async () => {
263
- let callCount = 0;
264
- mockPage.evaluate = mock(async () => {
265
- callCount++;
266
- if (callCount === 1) return "Page text";
267
- return [
268
- { text: "About", href: "https://example.com/about" },
269
- { text: "Contact", href: "https://example.com/contact" },
270
- ];
271
- });
384
+ test("includes links when requested using EXTRACT_LINKS_EXPRESSION", async () => {
385
+ sendHandler = (method, params) => {
386
+ if (method !== "Runtime.evaluate") return {};
387
+ const expression = (params as { expression: string }).expression;
388
+ if (expression === "document.location.href") {
389
+ return { result: { value: "https://example.com/" } };
390
+ }
391
+ if (expression === "document.title") {
392
+ return { result: { value: "Test Page" } };
393
+ }
394
+ if (expression === "document.body?.innerText ?? ''") {
395
+ return { result: { value: "Page text" } };
396
+ }
397
+ if (expression === EXTRACT_LINKS_EXPRESSION) {
398
+ return {
399
+ result: {
400
+ value: [
401
+ { text: "About", href: "https://example.com/about" },
402
+ { text: "Contact", href: "https://example.com/contact" },
403
+ ],
404
+ },
405
+ };
406
+ }
407
+ return { result: { value: null } };
408
+ };
272
409
 
273
410
  const result = await executeBrowserExtract({ include_links: true }, ctx);
274
411
  expect(result.isError).toBe(false);
275
412
  expect(result.content).toContain("Links:");
276
413
  expect(result.content).toContain("[About](https://example.com/about)");
277
414
  expect(result.content).toContain("[Contact](https://example.com/contact)");
415
+ // EXTRACT_LINKS_EXPRESSION was actually used (assert the expression appears in a call)
416
+ const linksCall = sendCalls.find(
417
+ (c) =>
418
+ c.method === "Runtime.evaluate" &&
419
+ (c.params as { expression: string }).expression ===
420
+ EXTRACT_LINKS_EXPRESSION,
421
+ );
422
+ expect(linksCall).toBeDefined();
278
423
  });
279
424
 
280
425
  test("does not include links by default", async () => {
281
- mockPage.evaluate = mock(async () => "Page text");
426
+ sendHandler = (method, params) => {
427
+ if (method !== "Runtime.evaluate") return {};
428
+ const expression = (params as { expression: string }).expression;
429
+ if (expression === "document.location.href") {
430
+ return { result: { value: "https://example.com/" } };
431
+ }
432
+ if (expression === "document.title") {
433
+ return { result: { value: "Test Page" } };
434
+ }
435
+ return { result: { value: "Page text" } };
436
+ };
437
+
282
438
  const result = await executeBrowserExtract({}, ctx);
283
439
  expect(result.content).not.toContain("Links:");
440
+ // EXTRACT_LINKS_EXPRESSION should NOT have been evaluated
441
+ const linksCall = sendCalls.find(
442
+ (c) =>
443
+ c.method === "Runtime.evaluate" &&
444
+ (c.params as { expression: string }).expression ===
445
+ EXTRACT_LINKS_EXPRESSION,
446
+ );
447
+ expect(linksCall).toBeUndefined();
284
448
  });
285
449
 
286
- test("handles extract error", async () => {
287
- mockPage.evaluate = mock(async () => {
288
- throw new Error("page crashed");
289
- });
450
+ test("surfaces CDP failure as an extract error", async () => {
451
+ sendHandler = () => new Error("page crashed");
452
+
290
453
  const result = await executeBrowserExtract({}, ctx);
291
454
  expect(result.isError).toBe(true);
292
455
  expect(result.content).toContain("Extract failed");