@wenathlan/devthink 2.0.12 → 2.0.14

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 +20 -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,206 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ canceltask,
4
+ claim,
5
+ claimheartbeat,
6
+ complete,
7
+ emptyqueue,
8
+ enqueue,
9
+ lanereport,
10
+ queuecomplete,
11
+ requeue,
12
+ steal,
13
+ taskcounts,
14
+ } from "../swarm.js";
15
+ import type { taskqueue } from "../types.js";
16
+
17
+ const now = 1_800_000_000_000;
18
+
19
+ /** Builds one queue fixture with the user configured lanes and priorities. */
20
+ function queue(over: Partial<taskqueue> = {}): taskqueue {
21
+ return {
22
+ lanes: ["extraction", "review"],
23
+ priorities: [1, 2, 3],
24
+ completionpolicy: "all",
25
+ items: [],
26
+ claims: [],
27
+ ...over,
28
+ };
29
+ }
30
+
31
+ describe("taskqueue enqueue claim complete", () => {
32
+ it("enqueues a task item into a lane with its priority", () => {
33
+ const withtask = enqueue({
34
+ queue: queue(),
35
+ id: "t1",
36
+ lane: "extraction",
37
+ priority: 2,
38
+ payload: "Read the pricing table",
39
+ now,
40
+ });
41
+ expect(withtask.items).toHaveLength(1);
42
+ expect(withtask.items[0]).toMatchObject({ id: "t1", lane: "extraction", priority: 2, state: "queued" });
43
+ });
44
+
45
+ it("refuses a task outside the configured lanes or without a payload", () => {
46
+ expect(() => enqueue({ queue: queue(), id: "t1", lane: "unknown", priority: 1, payload: "Task", now })).toThrow(
47
+ /configured lanes/i,
48
+ );
49
+ expect(() => enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: " ", now })).toThrow(
50
+ /payload/i,
51
+ );
52
+ expect(() => enqueue({ queue: queue(), id: "t1", lane: " ", priority: 1, payload: "Task", now })).toThrow(/lane/i);
53
+ });
54
+
55
+ it("accepts any lane when the user configured no lanes at all", () => {
56
+ const open = enqueue({ queue: emptyqueue(), id: "t1", lane: "anywhere", priority: 0, payload: "Task", now });
57
+ expect(open.items[0]?.lane).toBe("anywhere");
58
+ });
59
+
60
+ it("lets one agent claim the highest priority task and complete releases the claim", () => {
61
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "Low priority", now });
62
+ state = enqueue({ queue: state, id: "t2", lane: "extraction", priority: 3, payload: "High priority", now });
63
+ const claimed = claim({ queue: state, agentid: "a1", now });
64
+ expect(claimed.task?.id).toBe("t2");
65
+ expect(claimed.queue.claims[0]).toMatchObject({ agentid: "a1", taskid: "t2" });
66
+ expect(claim({ queue: claimed.queue, agentid: "a1", now: now + 1 }).task?.id).toBe("t2");
67
+ const completed = complete({ queue: claimed.queue, taskid: "t2", now: now + 2 });
68
+ expect(completed.items.find((item) => item.id === "t2")?.state).toBe("done");
69
+ expect(completed.claims).toHaveLength(0);
70
+ const next = claim({ queue: completed, agentid: "a2", now: now + 3 });
71
+ expect(next.task?.id).toBe("t1");
72
+ });
73
+
74
+ it("refuses the completion of a task that is not claimed", () => {
75
+ const state = enqueue({ queue: queue(), id: "t1", lane: "review", priority: 1, payload: "Check", now });
76
+ expect(() => complete({ queue: state, taskid: "t1", now })).toThrow(/only a claimed task/i);
77
+ expect(() => complete({ queue: state, taskid: "missing", now })).toThrow(/not in the queue/i);
78
+ });
79
+ });
80
+
81
+ describe("taskqueue priorities and ordering", () => {
82
+ it("orders the claims by priority with the oldest task winning a tie", () => {
83
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 2, payload: "Older", now });
84
+ state = enqueue({ queue: state, id: "t2", lane: "extraction", priority: 2, payload: "Newer", now: now + 1 });
85
+ state = enqueue({ queue: state, id: "t3", lane: "extraction", priority: 1, payload: "Lower", now: now + 2 });
86
+ expect(claim({ queue: state, agentid: "a1", now }).task?.id).toBe("t1");
87
+ const completed = complete({ queue: claim({ queue: state, agentid: "a1", now }).queue, taskid: "t1", now });
88
+ expect(claim({ queue: completed, agentid: "a2", now }).task?.id).toBe("t2");
89
+ });
90
+ });
91
+
92
+ describe("taskqueue work stealing", () => {
93
+ it("lets an idle agent steal a queued task from a busy lane", () => {
94
+ let state = enqueue({ queue: queue(), id: "t1", lane: "review", priority: 3, payload: "Busy lane task", now });
95
+ const stolen = steal({ queue: state, agentid: "a2", role: "worker", fromlane: "review", now });
96
+ expect(stolen.task?.id).toBe("t1");
97
+ expect(stolen.queue.claims[0]).toMatchObject({ agentid: "a2", taskid: "t1" });
98
+ });
99
+
100
+ it("respects the lane ownership rules the user configures", () => {
101
+ const state = enqueue({ queue: queue(), id: "t1", lane: "review", priority: 3, payload: "Owned lane task", now });
102
+ const ownership = [{ lane: "review", roles: ["planner"] }];
103
+ expect(() => steal({ queue: state, agentid: "a1", role: "worker", fromlane: "review", ownership, now })).toThrow(
104
+ /roles planner/i,
105
+ );
106
+ const planned = steal({ queue: state, agentid: "a2", role: "planner", fromlane: "review", ownership, now });
107
+ expect(planned.task?.id).toBe("t1");
108
+ });
109
+
110
+ it("steals nothing from an empty lane", () => {
111
+ const stolen = steal({ queue: queue(), agentid: "a1", role: "worker", fromlane: "review", now });
112
+ expect(stolen.task).toBeUndefined();
113
+ expect(stolen.queue.items).toHaveLength(0);
114
+ });
115
+ });
116
+
117
+ describe("taskqueue heartbeat expiry and requeue", () => {
118
+ it("refreshes the claim heartbeats of one agent", () => {
119
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "Task", now });
120
+ state = claim({ queue: state, agentid: "a1", now }).queue;
121
+ const beat = claimheartbeat({ queue: state, agentid: "a1", now: now + 4_000 });
122
+ expect(beat.claims[0]?.heartbeatat).toBe(now + 4_000);
123
+ });
124
+
125
+ it("requeues the orphaned tasks whose claims expired past the window", () => {
126
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "Orphan", now });
127
+ state = claim({ queue: state, agentid: "a1", now }).queue;
128
+ state = enqueue({ queue: state, id: "t2", lane: "extraction", priority: 1, payload: "Live", now });
129
+ state = claim({ queue: state, agentid: "a2", now: now + 6_000 }).queue;
130
+ const expired = requeue({ queue: state, now: now + 10_000, window: 5_000 });
131
+ expect(expired.requeued).toEqual(["t1"]);
132
+ expect(expired.queue.items.find((item) => item.id === "t1")?.state).toBe("queued");
133
+ expect(expired.queue.items.find((item) => item.id === "t2")?.state).toBe("claimed");
134
+ expect(expired.queue.claims.map((record) => record.taskid)).toEqual(["t2"]);
135
+ });
136
+
137
+ it("never expires a claim when no window is configured", () => {
138
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "Held", now });
139
+ state = claim({ queue: state, agentid: "a1", now }).queue;
140
+ const outcome = requeue({ queue: state, now: now + 1_000_000 });
141
+ expect(outcome.requeued).toEqual([]);
142
+ expect(outcome.queue.items[0]?.state).toBe("claimed");
143
+ });
144
+ });
145
+
146
+ describe("taskqueue reports and policies", () => {
147
+ it("reports every lane with its task counts and live claims", () => {
148
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "One", now });
149
+ state = enqueue({ queue: state, id: "t2", lane: "review", priority: 2, payload: "Two", now });
150
+ state = claim({ queue: state, agentid: "a1", now }).queue;
151
+ const report = lanereport(state);
152
+ expect(report.find((lane) => lane.lane === "extraction")).toMatchObject({
153
+ queued: 1,
154
+ claimed: 0,
155
+ done: 0,
156
+ cancelled: 0,
157
+ });
158
+ expect(report.find((lane) => lane.lane === "review")).toMatchObject({ queued: 0, claimed: 1 });
159
+ expect(report.find((lane) => lane.lane === "review")?.claims[0]?.agentid).toBe("a1");
160
+ });
161
+
162
+ it("counts the tasks by claim state for the popup gauge", () => {
163
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "One", now });
164
+ state = enqueue({ queue: state, id: "t2", lane: "review", priority: 1, payload: "Two", now });
165
+ state = claim({ queue: state, agentid: "a1", now }).queue;
166
+ expect(taskcounts(state)).toEqual({ queued: 1, claimed: 1, done: 0, cancelled: 0 });
167
+ });
168
+
169
+ it("evaluates the completion policy of the queue", () => {
170
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "One", now });
171
+ state = enqueue({ queue: state, id: "t2", lane: "review", priority: 1, payload: "Two", now });
172
+ expect(queuecomplete(state)).toBe(false);
173
+ const anypolicy = enqueue({
174
+ queue: emptyqueue({ completionpolicy: "any" }),
175
+ id: "t1",
176
+ lane: "extraction",
177
+ priority: 1,
178
+ payload: "Only",
179
+ now,
180
+ });
181
+ expect(queuecomplete(anypolicy)).toBe(false);
182
+ const claimedany = claim({ queue: anypolicy, agentid: "a1", now }).queue;
183
+ expect(queuecomplete(complete({ queue: claimedany, taskid: "t1", now }))).toBe(true);
184
+ });
185
+
186
+ it("cancels a queued or claimed task and never a done one", () => {
187
+ let state = enqueue({ queue: queue(), id: "t1", lane: "extraction", priority: 1, payload: "Cancel me", now });
188
+ state = canceltask({ queue: state, taskid: "t1", now });
189
+ expect(state.items[0]?.state).toBe("cancelled");
190
+ state = enqueue({ queue: state, id: "t2", lane: "extraction", priority: 1, payload: "Claim then cancel", now });
191
+ const claimed = claim({ queue: state, agentid: "a1", now }).queue;
192
+ const cancelled = canceltask({ queue: claimed, taskid: "t2", now });
193
+ expect(cancelled.items.find((item) => item.id === "t2")?.state).toBe("cancelled");
194
+ expect(cancelled.claims).toHaveLength(0);
195
+ const finished = complete({
196
+ queue: claim({
197
+ queue: enqueue({ queue: cancelled, id: "t3", lane: "review", priority: 1, payload: "Finish me", now }),
198
+ agentid: "a2",
199
+ now,
200
+ }).queue,
201
+ taskid: "t3",
202
+ now: now + 1,
203
+ });
204
+ expect(() => canceltask({ queue: finished, taskid: "t3", now: now + 2 })).toThrow(/never cancels/i);
205
+ });
206
+ });
@@ -0,0 +1,336 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ applymock,
4
+ applyratelimit,
5
+ begincall,
6
+ batchrisk,
7
+ checkidempotency,
8
+ dryruntool,
9
+ endcall,
10
+ expireidempotency,
11
+ maptoolerror,
12
+ recordidempotency,
13
+ callretryhintof,
14
+ runbatch,
15
+ structurederrorof,
16
+ } from "../tools.js";
17
+ import { buildtoolcatalog } from "../tools.js";
18
+ import type { batchmember, callratelimit, clientrecord, idempotencyrecord, toolmock } from "../types.js";
19
+
20
+ const now = 1_800_000_000_000;
21
+ const catalog = buildtoolcatalog();
22
+ const pairedclient: clientrecord = { id: "client1", transport: "stdio", paired: true, connectedat: now - 1000 };
23
+ const session = {
24
+ id: "sess",
25
+ tabid: 7,
26
+ origin: "https://example.com",
27
+ startedat: now - 5000,
28
+ expiresat: now + 600_000,
29
+ grants: ["https://example.com"],
30
+ };
31
+ const plan = {
32
+ id: "plan1",
33
+ objective: "Run the reviewed tool calls",
34
+ origin: "https://example.com",
35
+ steps: [
36
+ {
37
+ id: "s1",
38
+ kind: "click" as const,
39
+ target: "#go",
40
+ summary: "Click the reviewed button",
41
+ risk: "sensitive" as const,
42
+ },
43
+ ],
44
+ createdat: now - 4000,
45
+ expiresat: now + 600_000,
46
+ state: "approved" as const,
47
+ };
48
+
49
+ /** Builds one per client rate limit record. */
50
+ function limit(over: Partial<callratelimit> = {}): callratelimit {
51
+ return { clientid: "client1", windowms: 60_000, budget: 2, windowstartedat: now, used: 0, ...over };
52
+ }
53
+
54
+ describe("toolcalls rate limits", () => {
55
+ it("counts calls per client per window and answers excess with the retry after", () => {
56
+ const first = applyratelimit({ limits: [limit()], clientid: "client1", now });
57
+ expect(first.allowed).toBe(true);
58
+ expect(first.used).toBe(1);
59
+ const second = applyratelimit({ limits: first.limits, clientid: "client1", now: now + 1000 });
60
+ expect(second.allowed).toBe(true);
61
+ expect(second.used).toBe(2);
62
+ const excess = applyratelimit({ limits: second.limits, clientid: "client1", now: now + 2000 });
63
+ expect(excess.allowed).toBe(false);
64
+ expect(excess.budget).toBe(2);
65
+ expect(excess.retryafter).toBe(58_000);
66
+ expect(excess.used).toBe(2);
67
+ const reset = applyratelimit({ limits: second.limits, clientid: "client1", now: now + 61_000 });
68
+ expect(reset.allowed).toBe(true);
69
+ expect(reset.used).toBe(1);
70
+ });
71
+
72
+ it("keeps absent limits and budgets unbounded with no silent default", () => {
73
+ expect(applyratelimit({ limits: [], clientid: "client1", now }).allowed).toBe(true);
74
+ const unbounded = applyratelimit({
75
+ limits: [{ clientid: "client1", windowms: 60_000, windowstartedat: now, used: 5 }],
76
+ clientid: "client1",
77
+ now,
78
+ });
79
+ expect(unbounded.allowed).toBe(true);
80
+ expect(unbounded.budget).toBeUndefined();
81
+ const other = applyratelimit({
82
+ limits: [limit({ clientid: "client2", budget: 1, used: 1 })],
83
+ clientid: "client1",
84
+ now,
85
+ });
86
+ expect(other.allowed).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe("toolcalls structured errors and retry hints", () => {
91
+ it("classifies retryable timeouts, busy windows and consent refusals", () => {
92
+ expect(callretryhintof({ code: "internal", message: "The page executor timed out." })).toBe("retry");
93
+ expect(callretryhintof({ message: "network went away" })).toBe("retry");
94
+ expect(callretryhintof({ message: "The session queue is busy." })).toBe("wait");
95
+ expect(callretryhintof({ code: "ratelimited", message: "The budget is spent." })).toBe("wait");
96
+ expect(callretryhintof({ code: "consentrefused", message: "The consent gates refused." })).toBe("none");
97
+ expect(callretryhintof({ message: "The client is unpaired." })).toBe("none");
98
+ expect(callretryhintof({ message: "The origin stays outside the grants." })).toBe("none");
99
+ });
100
+
101
+ it("maps executor failures onto structured errors with their retry hints", () => {
102
+ const timeout = maptoolerror({ failure: { code: "internal", message: "The page executor timed out." } });
103
+ expect(timeout).toMatchObject({ code: "internal", retryhint: "retry" });
104
+ const consent = maptoolerror({
105
+ failure: { code: "consentrefused", message: "The consent gates refused the tool call." },
106
+ });
107
+ expect(consent.retryhint).toBe("none");
108
+ const error = maptoolerror({ failure: new Error("The bridge is busy.") });
109
+ expect(error.code).toBe("internal");
110
+ expect(error.retryhint).toBe("wait");
111
+ expect(
112
+ structurederrorof({ code: "ratelimited", message: "The budget is spent.", retryhint: "wait", retryafter: 4000 })
113
+ .retryafter,
114
+ ).toBe(4000);
115
+ });
116
+ });
117
+
118
+ describe("toolcalls idempotency", () => {
119
+ it("replays the stored result for a repeated key of the same client", () => {
120
+ const records = recordidempotency({
121
+ records: [],
122
+ key: "key-1",
123
+ clientid: "client1",
124
+ tool: "browser.readtext",
125
+ result: { content: "the stored answer", iserror: false },
126
+ now,
127
+ window: 60_000,
128
+ });
129
+ const replay = checkidempotency({ records, key: "key-1", clientid: "client1", now: now + 1000 });
130
+ expect(replay.replay?.content).toBe("the stored answer");
131
+ expect(replay.record?.createdat).toBe(now);
132
+ expect(checkidempotency({ records, key: "key-2", clientid: "client1", now }).reason).toMatch(
133
+ /names no stored record/i,
134
+ );
135
+ expect(checkidempotency({ records, key: "key-1", clientid: "client2", now: now + 1000 }).reason).toMatch(
136
+ /another client/i,
137
+ );
138
+ });
139
+
140
+ it("expires the records past their window so the keys never replay", () => {
141
+ const records = recordidempotency({
142
+ records: [],
143
+ key: "key-1",
144
+ clientid: "client1",
145
+ tool: "browser.readtext",
146
+ result: { content: "stored", iserror: false },
147
+ now,
148
+ window: 5000,
149
+ });
150
+ expect(checkidempotency({ records, key: "key-1", clientid: "client1", now: now + 4999 }).replay).toBeDefined();
151
+ expect(checkidempotency({ records, key: "key-1", clientid: "client1", now: now + 5000 }).reason).toMatch(
152
+ /expired/i,
153
+ );
154
+ const live: idempotencyrecord = {
155
+ key: "key-2",
156
+ clientid: "client1",
157
+ tool: "memory.list",
158
+ result: { content: "stored", iserror: false },
159
+ createdat: now,
160
+ expiresat: now + 10_000,
161
+ };
162
+ expect(expireidempotency([records[0] as idempotencyrecord, live], now + 6000)).toEqual([live]);
163
+ const overwritten = recordidempotency({
164
+ records,
165
+ key: "key-1",
166
+ clientid: "client1",
167
+ tool: "browser.readtext",
168
+ result: { content: "new answer", iserror: false },
169
+ now: now + 1000,
170
+ window: 5000,
171
+ });
172
+ expect(overwritten).toHaveLength(1);
173
+ expect(overwritten[0]?.result.content).toBe("new answer");
174
+ });
175
+ });
176
+
177
+ describe("toolcalls batch execution", () => {
178
+ /** Builds one ordered batch member. */
179
+ function member(id: string, name: string, over: Record<string, unknown> = {}): batchmember {
180
+ return { id, name, params: { ...over } };
181
+ }
182
+
183
+ it("runs the ordered members and stops on the first error when the flag requests it", async () => {
184
+ const calls = [
185
+ member("m1", "browser.readtext", { target: "#title" }),
186
+ member("m2", "browser.readtext", { target: "#body" }),
187
+ member("m3", "memory.list"),
188
+ ];
189
+ const order: string[] = [];
190
+ const outcome = await runbatch({
191
+ calls,
192
+ stoponerror: true,
193
+ now,
194
+ execute: async (call) => {
195
+ order.push(call.id);
196
+ return call.id === "m2"
197
+ ? {
198
+ ok: false,
199
+ error: structurederrorof({
200
+ code: "internal",
201
+ message: "The page executor timed out.",
202
+ retryhint: "retry",
203
+ }),
204
+ }
205
+ : { ok: true, result: { content: `ran ${call.name}`, iserror: false } };
206
+ },
207
+ });
208
+ expect(order).toEqual(["m1", "m2"]);
209
+ expect(outcome.stoppedat).toBe("m2");
210
+ expect(outcome.outcomes).toHaveLength(2);
211
+ expect(outcome.outcomes[1]?.error?.retryhint).toBe("retry");
212
+ const keepgoing = await runbatch({
213
+ calls,
214
+ stoponerror: false,
215
+ now,
216
+ execute: async (call) =>
217
+ call.id === "m2" ? { ok: false } : { ok: true, result: { content: `ran ${call.name}`, iserror: false } },
218
+ });
219
+ expect(keepgoing.stoppedat).toBeUndefined();
220
+ expect(keepgoing.outcomes).toHaveLength(3);
221
+ });
222
+
223
+ it("grades the batch by its most sensitive member", () => {
224
+ expect(batchrisk([{ risk: "read" }, { risk: "read" }])).toBe("read");
225
+ expect(batchrisk([{ risk: "read" }, { risk: "sensitive" }])).toBe("sensitive");
226
+ expect(batchrisk([])).toBe("read");
227
+ });
228
+ });
229
+
230
+ describe("toolcalls dry runs and mocks", () => {
231
+ it("evaluates arguments and consent without side effects", () => {
232
+ const tool = catalog.domains
233
+ .flatMap((domain) => domain.tools)
234
+ .find((candidate) => candidate.name === "browser.click");
235
+ expect(tool).toBeDefined();
236
+ const dry = dryruntool({
237
+ tool: tool as never,
238
+ params: { stepid: "s1" },
239
+ client: pairedclient,
240
+ session,
241
+ plan,
242
+ origin: session.origin,
243
+ now,
244
+ });
245
+ expect(dry.argsvalid).toBe(true);
246
+ expect(dry.consentok).toBe(true);
247
+ expect(dry.executed).toBe(false);
248
+ expect(dry.mutations).toEqual([]);
249
+ const refused = dryruntool({
250
+ tool: tool as never,
251
+ params: {},
252
+ client: { ...pairedclient, paired: false },
253
+ session,
254
+ plan,
255
+ origin: session.origin,
256
+ now,
257
+ });
258
+ expect(refused.consentok).toBe(false);
259
+ expect(refused.argsvalid).toBe(false);
260
+ expect(refused.findings.join(" ")).toMatch(/required argument stepid/i);
261
+ const read = catalog.domains
262
+ .flatMap((domain) => domain.tools)
263
+ .find((candidate) => candidate.name === "browser.readtext");
264
+ const wrongtype = dryruntool({
265
+ tool: read as never,
266
+ params: { target: 42 },
267
+ client: pairedclient,
268
+ session,
269
+ plan,
270
+ origin: session.origin,
271
+ now,
272
+ });
273
+ expect(wrongtype.findings.join(" ")).toMatch(/carries a number value where the schema asks a string/i);
274
+ expect(wrongtype.argsvalid).toBe(false);
275
+ });
276
+
277
+ it("answers with canned results in test contexts and never outside them", () => {
278
+ const mock: toolmock = {
279
+ tool: "browser.readtext",
280
+ result: { content: "the canned read", iserror: false },
281
+ testcontext: true,
282
+ createdat: now,
283
+ };
284
+ const answered = applymock({ mocks: [mock], tool: "browser.readtext" });
285
+ expect(answered.result?.content).toBe("the canned read");
286
+ expect(applymock({ mocks: [], tool: "browser.readtext" }).result).toBeUndefined();
287
+ const real: toolmock = {
288
+ tool: "browser.click",
289
+ result: { content: "canned click", iserror: false },
290
+ testcontext: false,
291
+ createdat: now,
292
+ };
293
+ expect(applymock({ mocks: [real], tool: "browser.click" }).reason).toMatch(/outside a test context/i);
294
+ });
295
+ });
296
+
297
+ describe("toolcalls call contexts", () => {
298
+ it("isolates concurrent client state and closes with the outcome", () => {
299
+ const one = begincall({ clientid: "client1", tool: "browser.readtext", idempotencykey: "key-1", now });
300
+ const two = begincall({ clientid: "client2", tool: "memory.list", now: now + 1 });
301
+ const three = begincall({
302
+ clientid: "client1",
303
+ tool: "browser.click",
304
+ dryrun: true,
305
+ batchid: "batch1",
306
+ now: now + 2,
307
+ });
308
+ expect(one.clientid).not.toBe(two.clientid);
309
+ expect(one.callid).not.toBe(two.callid);
310
+ expect(one.chunks).toBe(0);
311
+ expect(one.idempotencykey).toBe("key-1");
312
+ expect(three.dryrun).toBe(true);
313
+ expect(three.batchid).toBe("batch1");
314
+ expect(
315
+ begincall({ clientid: "client1", tool: "browser.readtext", idempotencykey: " ", now }).idempotencykey,
316
+ ).toBeUndefined();
317
+ const closed = endcall({ contexts: [one, two], callid: one.callid, ok: true, now: now + 50 });
318
+ expect(closed.context?.state).toBe("done");
319
+ expect(closed.context?.endedat).toBe(now + 50);
320
+ const failed = endcall({
321
+ contexts: closed.contexts,
322
+ callid: two.callid,
323
+ ok: false,
324
+ errorcode: "internal",
325
+ partial: { content: "partial", iserror: false },
326
+ now: now + 60,
327
+ });
328
+ expect(failed.context?.state).toBe("failed");
329
+ expect(failed.context?.errorcode).toBe("internal");
330
+ expect(failed.context?.partial?.content).toBe("partial");
331
+ expect(endcall({ contexts: closed.contexts, callid: "missing", ok: false, now }).reason).toMatch(/never opened/i);
332
+ expect(endcall({ contexts: closed.contexts, callid: one.callid, ok: true, now: now + 70 }).context?.state).toBe(
333
+ "done",
334
+ );
335
+ });
336
+ });