@wenathlan/devthink 2.0.13 → 2.0.15

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 (444) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
@@ -0,0 +1,704 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ bindlocalhost,
4
+ connectclient,
5
+ defaultmcpconfig,
6
+ defaultmcpport,
7
+ disconnectclient,
8
+ dispatchtool,
9
+ enqueuerequest,
10
+ framedlog,
11
+ handleframe,
12
+ initialize,
13
+ launchbridge,
14
+ listtools,
15
+ localhostbind,
16
+ negotiate,
17
+ negotiatetoolfloor,
18
+ nextrequest,
19
+ pairclient,
20
+ parseframe,
21
+ parsewire,
22
+ ping,
23
+ relayframe,
24
+ restartbridge,
25
+ respond,
26
+ rpcerrorcodeof,
27
+ rpcerrornumbers,
28
+ rpcerrorof,
29
+ serializeframe,
30
+ servercapabilities,
31
+ servermethods,
32
+ toolcallevent,
33
+ validateframe,
34
+ wireformat,
35
+ } from "../mcp.js";
36
+ import { buildtoolcatalog } from "../tools.js";
37
+ import { toolcallframe, toolresultframe } from "../protocol.js";
38
+ import type { agentplan, agentsession, clientrecord, jsonrpcframe, tooldef } from "../types.js";
39
+
40
+ const now = 1_800_000_000_000;
41
+ const session: agentsession = {
42
+ id: "sess",
43
+ tabid: 7,
44
+ origin: "https://example.com",
45
+ startedat: now - 5000,
46
+ expiresat: now + 600_000,
47
+ grants: ["https://example.com"],
48
+ };
49
+ const clickstep = {
50
+ id: "s1",
51
+ kind: "click" as const,
52
+ target: "#go",
53
+ summary: "Click the reviewed button",
54
+ risk: "sensitive" as const,
55
+ };
56
+ const plan: agentplan = {
57
+ id: "plan1",
58
+ objective: "Run the reviewed tool calls",
59
+ origin: "https://example.com",
60
+ steps: [clickstep, { id: "s2", kind: "observe", summary: "Snapshot the tab", risk: "read" }],
61
+ createdat: now - 4000,
62
+ expiresat: now + 600_000,
63
+ state: "approved",
64
+ };
65
+ const catalog = buildtoolcatalog();
66
+ const config = { ...defaultmcpconfig(), enabled: true };
67
+ const pairedclient: clientrecord = {
68
+ id: "client1",
69
+ transport: "stdio",
70
+ paired: true,
71
+ connectedat: now - 1000,
72
+ pairedat: now - 500,
73
+ };
74
+
75
+ /** Builds one json rpc request frame. */
76
+ function requestframe(id: number | string, method: string, params?: Record<string, unknown>): jsonrpcframe {
77
+ return { jsonrpc: "2.0", id, method, ...(params !== undefined ? { params } : {}) };
78
+ }
79
+
80
+ describe("mcp frame grammar", () => {
81
+ it("round trips frames through serialize and parse with both wire formats", () => {
82
+ const frame = requestframe(1, "tools/call", { name: "browser.click", stepid: "s1" });
83
+ expect(parseframe(serializeframe(frame))).toEqual(frame);
84
+ expect(parseframe(wireformat(frame, "newline").trim())).toEqual(frame);
85
+ expect(parseframe(wireformat(frame, "httppost"))).toEqual(frame);
86
+ expect(parseframe(JSON.stringify({ transport: "http", frame }))).toEqual(frame);
87
+ const second = requestframe(2, "ping");
88
+ const block = `${wireformat(frame, "newline")}${wireformat(second, "newline")}`;
89
+ expect(parsewire(block)).toEqual([frame, second]);
90
+ expect(parsewire("\n\n")).toEqual([]);
91
+ });
92
+
93
+ it("rejects malformed wire input and frames with the rpc error codes", () => {
94
+ expect(() => parseframe("not json")).toThrow();
95
+ expect(() => parseframe("[1,2]")).toThrow();
96
+ const response = respond({ error: rpcerrorof("parse", "The wire frame does not parse as json.") });
97
+ expect(response.id).toBeNull();
98
+ expect(response.error?.code).toBe("parse");
99
+ expect(rpcerrornumbers).toEqual({
100
+ parse: -32700,
101
+ method: -32601,
102
+ params: -32602,
103
+ internal: -32603,
104
+ consentrefused: -32001,
105
+ });
106
+ for (const [code, number] of Object.entries(rpcerrornumbers) as Array<[keyof typeof rpcerrornumbers, number]>)
107
+ expect(rpcerrorcodeof(number)).toBe(code);
108
+ expect(rpcerrorcodeof(-32600)).toBeUndefined();
109
+ });
110
+
111
+ it("validates frame shape against the routing table and the user configured frame size", () => {
112
+ const methods = servermethods();
113
+ expect(methods.map((entry) => entry.method)).toEqual([
114
+ "initialize",
115
+ "ping",
116
+ "tools/list",
117
+ "negotiate",
118
+ "tools/call",
119
+ "prompts/list",
120
+ "prompts/call",
121
+ "calls/cancel",
122
+ ]);
123
+ expect(validateframe(requestframe(1, "ping"), methods)).toBeUndefined();
124
+ expect(validateframe({ ...requestframe(1, "ping"), jsonrpc: "1.0" as never }, methods)?.code).toBe("parse");
125
+ expect(
126
+ validateframe({ jsonrpc: "2.0", id: { nested: true } as unknown as string, method: "ping" }, methods)?.code,
127
+ ).toBe("parse");
128
+ expect(validateframe({ jsonrpc: "2.0", id: 1 }, methods)?.code).toBe("method");
129
+ expect(validateframe(requestframe(1, "nope/method"), methods)?.code).toBe("method");
130
+ expect(validateframe(requestframe(1, "ping", ["array"] as never), methods)?.code).toBe("params");
131
+ expect(validateframe(requestframe(1, "ping"), methods, { ...config, framesize: 10 })?.code).toBe("params");
132
+ expect(validateframe(requestframe(1, "ping"), methods, { ...config, framesize: 10_000 })).toBeUndefined();
133
+ });
134
+
135
+ it("emits framed logs that never carry payloads", () => {
136
+ expect(JSON.parse(framedlog("clientconnected", now, { clientid: "client1", transport: "stdio" }))).toEqual({
137
+ at: now,
138
+ event: "clientconnected",
139
+ clientid: "client1",
140
+ transport: "stdio",
141
+ });
142
+ expect(framedlog("toolcall", now)).not.toContain("params");
143
+ expect(framedlog("toolcall", now)).not.toContain("result");
144
+ });
145
+ });
146
+
147
+ describe("mcp handshake and negotiation", () => {
148
+ it("completes the initialize handshake and answers ping with pong", () => {
149
+ const handshake = initialize({ params: { protocolversion: "bogus" }, config, catalog });
150
+ expect(handshake.serverinfo.name).toBe("devthink");
151
+ expect(handshake.serverinfo.toolversion).toBe(catalog.version);
152
+ expect(handshake.serverinfo.tools).toBe(listtools(catalog).tools.length);
153
+ expect(handshake.serverinfo.namespaces).toEqual(["browser", "workflow", "memory", "system"]);
154
+ expect(handshake.protocolversion).toBe(handshake.serverinfo.protocolversion);
155
+ expect(handshake.instructions).toMatch(/review/i);
156
+ expect(ping({ now })).toEqual({ pong: true, at: now });
157
+ });
158
+
159
+ it("lists every tool with its version, risk grade, consent metadata and json schema inputs", () => {
160
+ const listing = listtools(catalog);
161
+ expect(listing.tools).toHaveLength(alltoolcount());
162
+ for (const tool of listing.tools) {
163
+ expect(tool.version).toBe(catalog.version);
164
+ expect(tool.inputschema.type).toBe("object");
165
+ expect(["read", "sensitive"]).toContain(tool.risk);
166
+ }
167
+ const run = listing.tools.find((tool) => tool.name === "workflow.run");
168
+ expect(run?.consentmeta?.review).toMatch(/run review/i);
169
+ expect(run?.consentmeta?.riskclass).toBe("sensitive");
170
+ expect(run?.consentmeta?.approvalrequired).toBe(true);
171
+ expect(run?.consentmeta?.originscope).toBe("session");
172
+ expect(listing.tools.find((tool) => tool.name === "browser.snapshot")?.consentmeta).toBeUndefined();
173
+ });
174
+
175
+ it("negotiates the per client tool floor and enforces the token scopes before the consent gates", async () => {
176
+ expect(negotiatetoolfloor(undefined, 1)).toEqual({ floor: 1 });
177
+ expect(negotiatetoolfloor(1, 1)).toEqual({ floor: 1 });
178
+ const highfloor = negotiatetoolfloor(2, 1);
179
+ expect("mismatch" in highfloor ? highfloor.mismatch : "").toMatch(/tool version floor 2/i);
180
+ const scoped = await dispatchtool({
181
+ params: { name: "memory.list" },
182
+ client: pairedclient,
183
+ catalog,
184
+ session,
185
+ plan,
186
+ origin: session.origin,
187
+ now,
188
+ scopes: ["browser"],
189
+ execute: async (step) => ({ content: `ran ${step.kind}`, iserror: false }),
190
+ });
191
+ expect(scoped.error?.code).toBe("consentrefused");
192
+ expect(scoped.error?.message).toMatch(/grants no memory tools/i);
193
+ const allowed = await dispatchtool({
194
+ params: { name: "memory.list" },
195
+ client: pairedclient,
196
+ catalog,
197
+ session,
198
+ plan,
199
+ origin: session.origin,
200
+ now,
201
+ scopes: ["memory"],
202
+ execute: async (step) => ({ content: `ran ${step.kind}`, iserror: false }),
203
+ });
204
+ expect(allowed.error).toBeUndefined();
205
+ const floorclient = { ...pairedclient, toolfloor: 1 };
206
+ expect(
207
+ (
208
+ await dispatchtool({
209
+ params: { name: "browser.readtext", target: "#title" },
210
+ client: floorclient,
211
+ catalog,
212
+ session,
213
+ plan,
214
+ origin: session.origin,
215
+ now,
216
+ execute: async () => ({ content: "ok", iserror: false }),
217
+ })
218
+ ).error,
219
+ ).toBeUndefined();
220
+ });
221
+
222
+ it("negotiates capability sets on the happy path and reports the mismatches", () => {
223
+ const server = servercapabilities({ config, catalog });
224
+ const agreed = negotiate({
225
+ client: { protocolmajor: 2, toolversion: server.toolversion, transports: ["stdio"] },
226
+ server,
227
+ });
228
+ expect(agreed.agreed).toBe(true);
229
+ expect(agreed.capabilities?.tools).toBe(server.tools);
230
+ expect(negotiate({ server }).agreed).toBe(true);
231
+ /* the 2.0.0 sunset closed the window: the numeric major one declaration refuses below the supported floor with the conversion path inside the mismatch, and the deprecated version one string parses nowhere so it answers the default */
232
+ expect(negotiate({ client: { protocolmajor: 1 }, server }).mismatch).toMatch(/below the supported floor/);
233
+ expect(negotiate({ client: { protocolmajor: 1 }, server }).mismatch).toMatch(/migrateplan/);
234
+ expect(negotiate({ client: { protocolversion: "1.1.54" }, server }).agreed).toBe(true);
235
+ expect(negotiate({ client: { toolversion: server.toolversion + 1 }, server }).mismatch).toMatch(/tool version/i);
236
+ expect(negotiate({ client: { transports: ["stdio", "carrierpigeon" as never] }, server }).mismatch).toMatch(
237
+ /transport/i,
238
+ );
239
+ });
240
+
241
+ it("keeps one clientrecord per transport with the pairing decision and the negotiated set", () => {
242
+ const first = connectclient({ transport: "stdio", now, id: "client1" });
243
+ const second = connectclient({ transport: "http", now: now + 1, id: "client2" });
244
+ expect(first.paired).toBe(false);
245
+ expect(second.transport).toBe("http");
246
+ const paired = pairclient([first, second], "client1", true, now + 2);
247
+ expect(paired[0]?.paired).toBe(true);
248
+ expect(paired[0]?.pairedat).toBe(now + 2);
249
+ const refused = pairclient(paired, "client2", false, now + 3);
250
+ expect(refused[1]?.paired).toBe(false);
251
+ expect(refused[1]?.disconnectedat).toBe(now + 3);
252
+ const disconnected = disconnectclient(paired, "client1", now + 4);
253
+ expect(disconnected[0]?.disconnectedat).toBe(now + 4);
254
+ expect(pairclient(disconnected, "client1", true, now + 5)[0]?.paired).toBe(true);
255
+ });
256
+ });
257
+
258
+ describe("mcp request queue and localhost binding", () => {
259
+ it("serializes request frames per client in arrival order under the user configured depth", () => {
260
+ const one = requestframe(1, "ping");
261
+ const two = requestframe(2, "ping");
262
+ const three = requestframe(3, "ping");
263
+ let queue = enqueuerequest({ queue: [], frame: one });
264
+ queue = enqueuerequest({ queue: queue ?? [], frame: two });
265
+ expect(queue).toEqual([one, two]);
266
+ expect(enqueuerequest({ queue: queue ?? [], frame: three })).toEqual([one, two, three]);
267
+ expect(enqueuerequest({ queue: queue ?? [], frame: three, depth: 3 })).toEqual([one, two, three]);
268
+ expect(enqueuerequest({ queue: [], frame: one, depth: 1 })).toEqual([one]);
269
+ const next = nextrequest(queue ?? []);
270
+ expect(next?.frame).toBe(one);
271
+ expect(next?.remaining).toEqual([two]);
272
+ expect(nextrequest([])).toBeUndefined();
273
+ });
274
+
275
+ it("binds the http listener to localhost by default and reports the bind state", () => {
276
+ expect(localhostbind).toBe("127.0.0.1");
277
+ expect(defaultmcpport).toBe(7436);
278
+ expect(bindlocalhost(config)).toEqual({ bind: "127.0.0.1", port: defaultmcpport, localhost: true });
279
+ expect(bindlocalhost({ ...config, bind: "" })).toEqual({
280
+ bind: "127.0.0.1",
281
+ port: defaultmcpport,
282
+ localhost: true,
283
+ });
284
+ expect(bindlocalhost({ ...config, bind: "localhost" }).localhost).toBe(true);
285
+ expect(bindlocalhost({ ...config, bind: "::1" }).localhost).toBe(true);
286
+ expect(bindlocalhost({ ...config, bind: "0.0.0.0" })).toEqual({
287
+ bind: "0.0.0.0",
288
+ port: defaultmcpport,
289
+ localhost: false,
290
+ });
291
+ expect(defaultmcpconfig().enabled).toBe(false);
292
+ expect(defaultmcpconfig().transports).toEqual(["stdio", "http"]);
293
+ });
294
+ });
295
+
296
+ describe("mcp tool dispatch behind the consent gates", () => {
297
+ /** Runs one tool call through the dispatch gate with a recording executor. */
298
+ async function calltool(
299
+ params: Record<string, unknown>,
300
+ client: clientrecord = pairedclient,
301
+ over: Partial<{
302
+ session: agentsession | undefined;
303
+ plan: agentplan | undefined;
304
+ origin: string;
305
+ tool: tooldef | undefined;
306
+ }> = {},
307
+ ): Promise<{
308
+ result?: { content: string; iserror: boolean };
309
+ error?: { code: string; message: string };
310
+ stepid?: string;
311
+ }> {
312
+ const executed: string[] = [];
313
+ const sessionarg = "session" in over ? over.session : session;
314
+ const planarg = "plan" in over ? over.plan : plan;
315
+ const outcome = await dispatchtool({
316
+ params,
317
+ client,
318
+ catalog,
319
+ ...(sessionarg !== undefined ? { session: sessionarg } : {}),
320
+ ...(planarg !== undefined ? { plan: planarg } : {}),
321
+ origin: over.origin ?? session.origin,
322
+ now,
323
+ execute: async (step) => {
324
+ executed.push(`${step.kind}:${step.id}`);
325
+ return { content: `ran ${step.kind}`, iserror: false };
326
+ },
327
+ });
328
+ return {
329
+ ...outcome,
330
+ ...(outcome.result !== undefined ? { result: outcome.result as { content: string; iserror: boolean } } : {}),
331
+ ...(outcome.error !== undefined ? { error: outcome.error } : {}),
332
+ ...(executed.length > 0 ? { stepid: executed[0] } : {}),
333
+ };
334
+ }
335
+
336
+ it("runs read only tools under the dryrun risk class once the session is approved", async () => {
337
+ const outcome = await calltool({ name: "browser.readtext", target: "#title" });
338
+ expect(outcome.error).toBeUndefined();
339
+ expect(outcome.result?.iserror).toBe(false);
340
+ expect(outcome.stepid).toContain("readtext");
341
+ const runs = await calltool({ name: "memory.list" });
342
+ expect(runs.error).toBeUndefined();
343
+ expect(runs.stepid).toContain("listruns");
344
+ });
345
+
346
+ it("runs a sensitive tool only as the approved plan step it names", async () => {
347
+ const outcome = await calltool({ name: "browser.click", stepid: "s1" });
348
+ expect(outcome.error).toBeUndefined();
349
+ expect(outcome.stepid).toBe("click:s1");
350
+ expect(await calltool({ name: "browser.click", stepid: "s2" })).toMatchObject({
351
+ error: { code: "consentrefused" },
352
+ });
353
+ expect(await calltool({ name: "browser.click" })).toMatchObject({ error: { code: "consentrefused" } });
354
+ expect(await calltool({ name: "browser.click", stepid: "missing" })).toMatchObject({
355
+ error: { code: "consentrefused" },
356
+ });
357
+ });
358
+
359
+ it("refuses unpaired clients, dead sessions, unapproved plans and origins outside the grants", async () => {
360
+ const unpaired = { ...pairedclient, paired: false };
361
+ expect(await calltool({ name: "browser.readtext", target: "#title" }, unpaired)).toMatchObject({
362
+ error: { code: "consentrefused" },
363
+ });
364
+ const disconnected = { ...pairedclient, disconnectedat: now };
365
+ expect(await calltool({ name: "browser.readtext", target: "#title" }, disconnected)).toMatchObject({
366
+ error: { code: "consentrefused" },
367
+ });
368
+ expect(
369
+ await calltool({ name: "browser.readtext", target: "#title" }, pairedclient, {
370
+ session: { ...session, stoppedat: now - 1 },
371
+ }),
372
+ ).toMatchObject({ error: { code: "consentrefused" } });
373
+ expect(
374
+ await calltool({ name: "browser.readtext", target: "#title" }, pairedclient, {
375
+ session: { ...session, expiresat: now - 1 },
376
+ }),
377
+ ).toMatchObject({ error: { code: "consentrefused" } });
378
+ expect(
379
+ await calltool({ name: "browser.readtext", target: "#title" }, pairedclient, {
380
+ plan: { ...plan, state: "pending" },
381
+ }),
382
+ ).toMatchObject({ error: { code: "consentrefused" } });
383
+ expect(
384
+ await calltool({ name: "browser.readtext", target: "#title" }, pairedclient, { session: undefined }),
385
+ ).toMatchObject({ error: { code: "consentrefused" } });
386
+ expect(
387
+ await calltool({ name: "browser.readtext", target: "#title" }, pairedclient, { origin: "https://other.example" }),
388
+ ).toMatchObject({ error: { code: "consentrefused" } });
389
+ });
390
+
391
+ it("refuses unknown tools, ambiguous bare names and versions below the negotiated floor", async () => {
392
+ expect(await calltool({})).toMatchObject({ error: { code: "params" } });
393
+ expect(await calltool({ name: "" })).toMatchObject({ error: { code: "params" } });
394
+ expect(await calltool({ name: "browser.nope" })).toMatchObject({ error: { code: "params" } });
395
+ expect(await calltool({ name: "list" })).toMatchObject({ error: { code: "params" } });
396
+ const oldfloor = {
397
+ ...pairedclient,
398
+ capabilities: {
399
+ protocolversion: "1.1.54",
400
+ name: "client",
401
+ version: "1",
402
+ toolversion: catalog.version + 1,
403
+ tools: 0,
404
+ namespaces: ["browser" as const],
405
+ transports: ["stdio" as const],
406
+ },
407
+ };
408
+ expect(await calltool({ name: "browser.readtext", target: "#title" }, oldfloor)).toMatchObject({
409
+ error: { code: "params" },
410
+ });
411
+ });
412
+
413
+ it("carries executor failures as internal errors and tool failures as error results", async () => {
414
+ const failing = await dispatchtool({
415
+ params: { name: "browser.readtext", target: "#title" },
416
+ client: pairedclient,
417
+ catalog,
418
+ session,
419
+ plan,
420
+ origin: session.origin,
421
+ now,
422
+ execute: () => {
423
+ throw new Error("bridge missing");
424
+ },
425
+ });
426
+ expect(failing.error?.code).toBe("internal");
427
+ const toolerror = await dispatchtool({
428
+ params: { name: "browser.readtext", target: "#title" },
429
+ client: pairedclient,
430
+ catalog,
431
+ session,
432
+ plan,
433
+ origin: session.origin,
434
+ now,
435
+ execute: async () => ({ content: "the read failed", iserror: true }),
436
+ });
437
+ expect(toolerror.result?.iserror).toBe(true);
438
+ expect(toolerror.error).toBeUndefined();
439
+ });
440
+ });
441
+
442
+ describe("mcp frame routing", () => {
443
+ /** Routes one frame through handleframe with a recording executor. */
444
+ async function route(
445
+ frame: jsonrpcframe,
446
+ client: clientrecord = pairedclient,
447
+ raw?: string,
448
+ configoverride: Partial<typeof config> = {},
449
+ ): Promise<jsonrpcframe> {
450
+ return await handleframe({
451
+ ...(raw !== undefined ? { raw } : { frame }),
452
+ client,
453
+ catalog,
454
+ config: { ...config, ...configoverride },
455
+ session,
456
+ plan,
457
+ origin: session.origin,
458
+ tabid: session.tabid,
459
+ now,
460
+ execute: async (step) => ({ content: `ran ${step.kind}`, iserror: false }),
461
+ });
462
+ }
463
+
464
+ it("routes initialize, ping, tools/list and negotiate through the method table", async () => {
465
+ const handshake = await route(requestframe(1, "initialize", { protocolversion: "1.1.54" }));
466
+ expect(handshake.id).toBe(1);
467
+ expect((handshake.result as { serverinfo?: { name?: string } }).serverinfo?.name).toBe("devthink");
468
+ const pong = await route(requestframe("alpha", "ping"));
469
+ expect(pong.id).toBe("alpha");
470
+ expect((pong.result as { pong?: boolean }).pong).toBe(true);
471
+ const listing = await route(requestframe(3, "tools/list"));
472
+ expect((listing.result as { tools?: unknown[] }).tools).toHaveLength(alltoolcount());
473
+ const caps = servercapabilities({ config, catalog });
474
+ const agreed = await route(
475
+ requestframe(4, "negotiate", {
476
+ capabilities: { protocolmajor: 2, toolversion: caps.toolversion, transports: ["stdio"] },
477
+ }),
478
+ );
479
+ expect((agreed.result as { tools?: number }).tools).toBe(caps.tools);
480
+ /* the 2.0.0 sunset: the numeric major one declaration refuses below the supported floor, so the negotiate route answers the params error with the conversion path inside the message */
481
+ const mismatch = await route(requestframe(5, "negotiate", { capabilities: { protocolmajor: 1 } }));
482
+ expect(mismatch.error?.code).toBe("params");
483
+ expect(mismatch.error?.message).toContain("migrateplan");
484
+ });
485
+
486
+ it("answers parse, method and consent failures with the matching error codes", async () => {
487
+ expect((await route({ jsonrpc: "2.0", id: 1 }, pairedclient, "not json")).error?.code).toBe("parse");
488
+ expect((await route(requestframe(1, "nope/method"))).error?.code).toBe("method");
489
+ expect(
490
+ (await route(requestframe(1, "tools/call", { name: "browser.click" }), { ...pairedclient, paired: false })).error
491
+ ?.code,
492
+ ).toBe("consentrefused");
493
+ const oversized = await route(
494
+ { jsonrpc: "2.0", id: 1 },
495
+ pairedclient,
496
+ serializeframe(requestframe(1, "ping")) + " ".repeat(200),
497
+ { framesize: 100 },
498
+ );
499
+ expect(oversized.error?.code).toBe("params");
500
+ expect(oversized.id).toBeNull();
501
+ expect(
502
+ (await route({ jsonrpc: "2.0", id: 1 }, pairedclient, serializeframe(requestframe(1, "ping")) + " ".repeat(200)))
503
+ .error,
504
+ ).toBeUndefined();
505
+ });
506
+
507
+ it("dispatches tool calls through the routing table and keeps the request id on the response", async () => {
508
+ const response = await route(requestframe(42, "tools/call", { name: "browser.click", stepid: "s1" }));
509
+ expect(response.id).toBe(42);
510
+ expect((response.result as { iserror?: boolean }).iserror).toBe(false);
511
+ const read = await route(requestframe(43, "tools/call", { name: "browser.snapshot" }));
512
+ expect((read.result as { content?: string }).content).toBe("ran observe");
513
+ });
514
+
515
+ it("routes the prompt tools and the in flight cancellation through the routing table", async () => {
516
+ const prompts = await route(requestframe(6, "prompts/list"));
517
+ const listed = prompts.result as { prompts?: Array<{ name: string; arguments: unknown[] }> };
518
+ expect(listed.prompts?.map((prompt) => prompt.name)).toEqual(["runreview", "pagesummary", "failuretriage"]);
519
+ expect(listed.prompts?.[0]?.arguments.length).toBeGreaterThan(0);
520
+ const called = await route(
521
+ requestframe(7, "prompts/call", {
522
+ name: "runreview",
523
+ arguments: { objective: "fill the form", steps: "click, type" },
524
+ }),
525
+ );
526
+ const rendered = called.result as { toolcall?: { name: string }; rendered?: string };
527
+ expect(rendered.toolcall?.name).toBe("prompts.runreview");
528
+ expect(rendered.rendered).toContain("fill the form");
529
+ const missing = await route(requestframe(8, "prompts/call", { name: "bogus" }));
530
+ expect(missing.error?.code).toBe("params");
531
+ const required = await route(
532
+ requestframe(9, "prompts/call", { name: "runreview", arguments: { steps: "click, type" } }),
533
+ );
534
+ expect(required.error?.code).toBe("params");
535
+ const inflight = await route(requestframe(10, "calls/cancel", { callid: "call-1", reason: "the user asked" }));
536
+ expect(inflight.error?.code).toBe("params");
537
+ const context = {
538
+ callid: "call-1",
539
+ clientid: pairedclient.id,
540
+ tool: "browser.readtext",
541
+ state: "inflight" as const,
542
+ startedat: now,
543
+ chunks: 1,
544
+ partial: { content: "the first chunks", iserror: false },
545
+ };
546
+ const aborted = await handleframe({
547
+ frame: requestframe(11, "calls/cancel", { callid: "call-1", reason: "the user asked" }),
548
+ client: pairedclient,
549
+ catalog,
550
+ config,
551
+ session,
552
+ plan,
553
+ origin: session.origin,
554
+ tabid: session.tabid,
555
+ now,
556
+ contexts: [context],
557
+ execute: async () => ({ content: "never runs", iserror: false }),
558
+ });
559
+ expect((aborted.result as { cancelled?: boolean }).cancelled).toBe(true);
560
+ expect((aborted.result as { partial?: { content: string } }).partial?.content).toBe("the first chunks");
561
+ const finished = { ...context, state: "done" as const, endedat: now };
562
+ expect(
563
+ (
564
+ await handleframe({
565
+ frame: requestframe(12, "calls/cancel", { callid: "call-1" }),
566
+ client: pairedclient,
567
+ catalog,
568
+ config,
569
+ session,
570
+ plan,
571
+ origin: session.origin,
572
+ tabid: session.tabid,
573
+ now,
574
+ contexts: [finished],
575
+ execute: async () => ({ content: "ok", iserror: false }),
576
+ })
577
+ ).error?.code,
578
+ ).toBe("params");
579
+ });
580
+
581
+ it("stamps the extended tool call records with the 1.1.56 markers", () => {
582
+ const event = toolcallevent({
583
+ id: "t2",
584
+ clientid: "client1",
585
+ tool: "browser.click",
586
+ origin: "https://example.com",
587
+ ok: true,
588
+ now,
589
+ callid: "call-1",
590
+ idempotencykey: "key-1",
591
+ dryrun: true,
592
+ mocked: false,
593
+ batchid: "batch-1",
594
+ replayed: true,
595
+ });
596
+ expect(event).toMatchObject({
597
+ callid: "call-1",
598
+ idempotencykey: "key-1",
599
+ dryrun: true,
600
+ batchid: "batch-1",
601
+ replayed: true,
602
+ });
603
+ expect(event.mocked).toBeUndefined();
604
+ expect(
605
+ toolcallevent({
606
+ id: "t3",
607
+ clientid: "client1",
608
+ tool: "browser.click",
609
+ origin: "https://example.com",
610
+ ok: false,
611
+ now,
612
+ code: "consentrefused",
613
+ }).callid,
614
+ ).toBeUndefined();
615
+ });
616
+
617
+ it("builds toolcall and toolresult frames through the protocol envelopes", () => {
618
+ const call = toolcallframe({ id: 7, name: "browser.click", params: { stepid: "s1" } });
619
+ expect(call.method).toBe("tools/call");
620
+ expect(call.params?.name).toBe("browser.click");
621
+ expect(call.params?.stepid).toBe("s1");
622
+ const result = toolresultframe({ id: 7, result: { content: "ran click", iserror: false } });
623
+ expect(result.result).toEqual({ content: "ran click", iserror: false });
624
+ const refusal = toolresultframe({
625
+ id: 7,
626
+ error: rpcerrorof("consentrefused", "The consent gates refused the tool call."),
627
+ });
628
+ expect(refusal.error?.code).toBe("consentrefused");
629
+ expect(
630
+ toolcallevent({
631
+ id: "t1",
632
+ clientid: "client1",
633
+ tool: "browser.click",
634
+ origin: "https://example.com",
635
+ ok: false,
636
+ now,
637
+ code: "consentrefused",
638
+ }).code,
639
+ ).toBe("consentrefused");
640
+ });
641
+ });
642
+
643
+ describe("mcp stdio bridge", () => {
644
+ it("launches through the native messaging host manifest and relays frames in both directions", () => {
645
+ const bridge = launchbridge({ host: "com.wenathlan.devthink", pid: 4242, now, id: "bridge1" });
646
+ expect(bridge).toMatchObject({
647
+ host: "com.wenathlan.devthink",
648
+ pid: 4242,
649
+ connected: true,
650
+ restarts: 0,
651
+ received: 0,
652
+ sent: 0,
653
+ });
654
+ expect(launchbridge({ host: "com.wenathlan.devthink", now: now + 1 }).pid).toBeUndefined();
655
+ const request = requestframe(1, "initialize");
656
+ const response = respond({ id: 1, result: { pong: true } });
657
+ const relaying = relayframe({ bridge, direction: "inbound", frame: request, now: now + 2 });
658
+ expect(relaying.received).toBe(1);
659
+ expect(relaying.sent).toBe(0);
660
+ expect(relaying.lastframeat).toBe(now + 2);
661
+ const answered = relayframe({ bridge: relaying, direction: "outbound", frame: response, now: now + 3 });
662
+ expect(answered.received).toBe(1);
663
+ expect(answered.sent).toBe(1);
664
+ });
665
+
666
+ it("verifies the client signature when the platform allows it and refuses a failed signature", () => {
667
+ /* the 1.1.95 hardening: the platform verifier confirms the presented signature before the bridge connects, a failed signature refuses the bridge outright and a platform without a verifier keeps the bridge unverified while the record says so */
668
+ const verified = launchbridge({
669
+ host: "com.wenathlan.devthink",
670
+ now,
671
+ signature: "signature-one",
672
+ verifier: () => true,
673
+ });
674
+ expect(verified.clientverified).toBe(true);
675
+ expect(() =>
676
+ launchbridge({ host: "com.wenathlan.devthink", now, signature: "forged-signature", verifier: () => false }),
677
+ ).toThrow("failed the platform verification");
678
+ const unverified = launchbridge({ host: "com.wenathlan.devthink", now, signature: "signature-one" });
679
+ expect(unverified.clientverified).toBeUndefined();
680
+ const verifierless = launchbridge({ host: "com.wenathlan.devthink", now, verifier: () => true });
681
+ expect(verifierless.clientverified).toBeUndefined();
682
+ });
683
+
684
+ it("restarts a dead client process on demand and keeps the counters", () => {
685
+ const bridge = launchbridge({ host: "com.wenathlan.devthink", pid: 4242, now });
686
+ const dead = {
687
+ ...relayframe({ bridge, direction: "inbound", frame: requestframe(1, "ping"), now: now + 1 }),
688
+ connected: false,
689
+ };
690
+ expect(dead.connected).toBe(false);
691
+ const restarted = restartbridge({ bridge: dead, pid: 5252, now: now + 2 });
692
+ expect(restarted.connected).toBe(true);
693
+ expect(restarted.pid).toBe(5252);
694
+ expect(restarted.restarts).toBe(1);
695
+ expect(restarted.received).toBe(1);
696
+ expect(restarted.startedat).toBe(now + 2);
697
+ expect(restartbridge({ bridge: restarted, pid: 6363, now: now + 3 }).restarts).toBe(2);
698
+ });
699
+ });
700
+
701
+ /** Counts the tools of the built catalog. */
702
+ function alltoolcount(): number {
703
+ return listtools(catalog).tools.length;
704
+ }