@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,626 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ acquirerunlock,
4
+ advancerun,
5
+ beatrun,
6
+ budgetalertstate,
7
+ cancelrollback,
8
+ checkpointisvalid,
9
+ closerun,
10
+ dequeueoffline,
11
+ enqueueoffline,
12
+ expirerunlocks,
13
+ heartbeatwindowof,
14
+ heartbeatisstale,
15
+ initialrun,
16
+ makecheckpoint,
17
+ openrun,
18
+ reattachrun,
19
+ recordenvironment,
20
+ recordturnaround,
21
+ recordurl,
22
+ recoveryplan,
23
+ releaserunlock,
24
+ replayqueue,
25
+ resumecheckpoint,
26
+ rollbackrun,
27
+ runbudgetof,
28
+ runcomplete,
29
+ runfail,
30
+ runfromplan,
31
+ runheartbeat,
32
+ runpausesforalerts,
33
+ runstatemachine,
34
+ serializesteps,
35
+ sideeffects,
36
+ stepkey,
37
+ timeoutcanceldecision,
38
+ transitionislegal,
39
+ zombiecheck,
40
+ zombiesweep,
41
+ } from "../run.js";
42
+ import type { agentplan, heartbeatrecord, planprogress, runrecord, runsettings, toolstep } from "../types.js";
43
+
44
+ const now = 1_800_000_000_000;
45
+
46
+ /** Builds one minimal approved plan fixture. */
47
+ function plan(over: Partial<agentplan> = {}): agentplan {
48
+ return {
49
+ id: "p1",
50
+ objective: "Check the pricing page",
51
+ origin: "https://example.com",
52
+ steps: [],
53
+ createdat: now - 1000,
54
+ expiresat: now + 60_000,
55
+ state: "approved",
56
+ ...over,
57
+ };
58
+ }
59
+
60
+ /** Builds one queued run fixture. */
61
+ function run(over: Partial<runrecord> = {}): runrecord {
62
+ return { runid: "r1", planid: "p1", sessionid: "s1", state: "queued", createdat: now, updatedat: now, ...over };
63
+ }
64
+
65
+ /** Builds one heartbeat fixture. */
66
+ function beat(over: Partial<heartbeatrecord> = {}): heartbeatrecord {
67
+ return { runid: "r1", beat: 1, at: now, ...over };
68
+ }
69
+
70
+ /** Builds one read only step fixture. */
71
+ function readstep(id: string, over: Partial<toolstep> = {}): toolstep {
72
+ return { id, kind: "readtext", summary: `read ${id}`, risk: "read", ...over };
73
+ }
74
+
75
+ /** Builds one mutating step fixture. */
76
+ function writestep(id: string, over: Partial<toolstep> = {}): toolstep {
77
+ return { id, kind: "type", target: "#field", value: "x", summary: `type ${id}`, risk: "interaction", ...over };
78
+ }
79
+
80
+ describe("torture: run lifecycle state machine", () => {
81
+ it("refuses every illegal transition from every terminal state", () => {
82
+ for (const to of [
83
+ "queued",
84
+ "running",
85
+ "paused",
86
+ "awaitingapproval",
87
+ "completed",
88
+ "failed",
89
+ "cancelled",
90
+ "rolledback",
91
+ ] as const) {
92
+ expect(() => advancerun(run({ state: "completed" }), to, now)).toThrow(/cannot move/i);
93
+ expect(() => advancerun(run({ state: "rolledback" }), to, now)).toThrow(/cannot move/i);
94
+ }
95
+ expect(() => advancerun(run({ state: "failed" }), "completed", now)).toThrow(/cannot move/i);
96
+ expect(() => advancerun(run({ state: "cancelled" }), "running", now)).toThrow(/cannot move/i);
97
+ expect(() => advancerun(run({ state: "failed" }), "failed", now)).toThrow(/cannot move/i);
98
+ });
99
+
100
+ it("accepts every legal transition the machine declares and only those", () => {
101
+ const machine = runstatemachine();
102
+ for (const [from, targets] of Object.entries(machine)) {
103
+ for (const to of targets) {
104
+ expect(transitionislegal({ from: from as never, to: to as never })).toBe(true);
105
+ const moved = advancerun(run({ state: from as never }), to, now);
106
+ expect(moved.state).toBe(to);
107
+ expect(moved.updatedat).toBe(now);
108
+ }
109
+ }
110
+ });
111
+
112
+ it("keeps the record immutable through transitions", () => {
113
+ const original = run();
114
+ const moved = advancerun(original, "running", now);
115
+ expect(original.state).toBe("queued");
116
+ expect(moved.runid).toBe("r1");
117
+ expect(moved).not.toBe(original);
118
+ });
119
+
120
+ it("refuses blank ids when creating runs and steps", () => {
121
+ expect(() => initialrun({ planid: "", sessionid: "s1", now })).toThrow(/plan id/i);
122
+ expect(() => initialrun({ planid: " ", sessionid: "s1", now })).toThrow(/plan id/i);
123
+ expect(() => initialrun({ planid: "p1", sessionid: "", now })).toThrow(/session id/i);
124
+ expect(() => stepkey({ planid: "", stepid: "s1", kind: "click" })).toThrow(/plan and step ids/i);
125
+ expect(() => stepkey({ planid: "p1", stepid: "", kind: "click" })).toThrow(/plan and step ids/i);
126
+ expect(() => stepkey({ planid: "p1", stepid: "s1", kind: " " })).toThrow(/step kind/i);
127
+ expect(stepkey({ planid: "p1", stepid: "s1", kind: "click" })).toBe("p1:s1:click");
128
+ expect(stepkey({ planid: "p:1", stepid: "s:1", kind: "navigate" })).toBe("p:1:s:1:navigate");
129
+ });
130
+
131
+ it("derives unique step keys across plans, steps and kinds", () => {
132
+ const keys = new Set<string>();
133
+ for (const planid of ["p1", "p2"]) {
134
+ for (const stepid of ["s1", "s2"]) {
135
+ for (const kind of ["click", "readtext"]) {
136
+ keys.add(stepkey({ planid, stepid, kind }));
137
+ }
138
+ }
139
+ }
140
+ expect(keys.size).toBe(8);
141
+ });
142
+ });
143
+
144
+ describe("torture: keepalive lifecycle and zombie reaping", () => {
145
+ it("refuses heartbeats and reattachment on stopped runs while closed runs stay frozen", () => {
146
+ const state = openrun({ runid: "r1", sessionid: "s1", planid: "p1", profileid: "u1", interval: 1_000, now });
147
+ const closed = closerun(state, now + 5_000);
148
+ expect(() => beatrun(closed, now + 6_000)).toThrow(/stopped/i);
149
+ expect(() => reattachrun(closed, now + 6_000)).toThrow(/stopped/i);
150
+ expect(() => closerun(closed, now + 7_000)).toThrow(/already stopped/i);
151
+ expect(closed.state).toBe("completed");
152
+ expect(closed.keepalive.portopen).toBe(false);
153
+ });
154
+
155
+ it("refuses negative and non finite intervals at open time", () => {
156
+ for (const interval of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
157
+ expect(() => openrun({ runid: "r1", sessionid: "s1", planid: "p1", profileid: "u1", interval, now })).toThrow(
158
+ /positive user value/i,
159
+ );
160
+ }
161
+ expect(() => openrun({ runid: "", sessionid: "s1", planid: "p1", profileid: "u1", interval: 1_000, now })).toThrow(
162
+ /run and session ids/i,
163
+ );
164
+ });
165
+
166
+ it("caps the heartbeat event trail at two hundred beats", () => {
167
+ let state = openrun({ runid: "r1", sessionid: "s1", planid: "p1", profileid: "u1", interval: 1_000, now });
168
+ for (let index = 0; index < 250; index += 1) state = beatrun(state, now + index * 1_000);
169
+ expect(state.keepalive.beats).toBe(250);
170
+ expect(state.keepalive.events.length).toBe(200);
171
+ expect(state.keepalive.lastbeatat).toBe(now + 249_000);
172
+ });
173
+
174
+ it("reaps exactly the runs whose silence passed the tolerance and only active ones", () => {
175
+ const base = openrun({ runid: "base", sessionid: "s1", planid: "p1", profileid: "u1", interval: 1_000, now });
176
+ const alive = { ...base, runid: "alive", keepalive: { ...base.keepalive, runid: "alive", lastbeatat: now } };
177
+ const silent = {
178
+ ...base,
179
+ runid: "silent",
180
+ keepalive: { ...base.keepalive, runid: "silent", lastbeatat: now - 10_000 },
181
+ };
182
+ const stopped = closerun({ ...base, runid: "stopped", keepalive: { ...base.keepalive, runid: "stopped" } }, now);
183
+ const alreadyreaped = { ...base, runid: "reapedalready", state: "reaped" as const };
184
+ const result = zombiesweep({
185
+ states: [alive, silent, stopped, alreadyreaped],
186
+ now,
187
+ interval: 1_000,
188
+ missedlimit: 3,
189
+ });
190
+ expect(result.reaped).toEqual(["silent"]);
191
+ expect(result.states.find((state) => state.runid === "silent")?.state).toBe("reaped");
192
+ expect(result.states.find((state) => state.runid === "alive")?.state).toBe("active");
193
+ expect(result.states.find((state) => state.runid === "stopped")?.state).toBe("completed");
194
+ expect(result.states.find((state) => state.runid === "reapedalready")?.state).toBe("reaped");
195
+ });
196
+
197
+ it("reaps at exactly the tolerated boundary plus one and never at the boundary", () => {
198
+ const base = openrun({ runid: "base", sessionid: "s1", planid: "p1", profileid: "u1", interval: 1_000, now });
199
+ const atboundary = { ...base, runid: "r1", keepalive: { ...base.keepalive, runid: "r1", lastbeatat: now - 3_000 } };
200
+ const overboundary = {
201
+ ...base,
202
+ runid: "over",
203
+ keepalive: { ...base.keepalive, runid: "over", lastbeatat: now - 3_001 },
204
+ };
205
+ const exactly = zombiesweep({ states: [atboundary], now, interval: 1_000, missedlimit: 3 });
206
+ expect(exactly.reaped).toEqual([]);
207
+ const over = zombiesweep({ states: [overboundary], now, interval: 1_000, missedlimit: 3 });
208
+ expect(over.reaped).toEqual(["over"]);
209
+ });
210
+
211
+ it("refuses invalid sweep parameters", () => {
212
+ expect(() => zombiesweep({ states: [], now, interval: 0, missedlimit: 3 })).toThrow(/positive user value/i);
213
+ expect(() => zombiesweep({ states: [], now, interval: 1_000, missedlimit: 0 })).toThrow(/positive whole number/i);
214
+ expect(() => zombiesweep({ states: [], now, interval: Number.NaN, missedlimit: 3 })).toThrow(
215
+ /positive user value/i,
216
+ );
217
+ expect(() => zombiesweep({ states: [], now, interval: 1_000, missedlimit: 1.5 })).toThrow(/positive whole number/i);
218
+ });
219
+
220
+ it("answers recovery for every terminal state and the pending step rule", () => {
221
+ const base = openrun({ runid: "r1", sessionid: "s1", planid: "p1", profileid: "u1", interval: 1_000, now });
222
+ expect(recoveryplan(closerun(base, now + 1)).recoverable).toBe(false);
223
+ expect(recoveryplan({ ...base, state: "reaped" }).recoverable).toBe(false);
224
+ expect(recoveryplan(base).recoverable).toBe(false);
225
+ expect(recoveryplan(base).reason).toMatch(/no pending step/i);
226
+ const pending = { ...base, pendingstepid: "s5" };
227
+ const recovery = recoveryplan(pending);
228
+ expect(recovery.recoverable).toBe(true);
229
+ expect(recovery.pendingstepid).toBe("s5");
230
+ });
231
+
232
+ it("records urls, environments and turnarounds with validation", () => {
233
+ let state = openrun({ runid: "r1", sessionid: "s1", planid: "p1", profileid: "u1", interval: 1_000, now });
234
+ expect(() => recordurl(state, { url: " ", stepid: "s1", now })).toThrow(/needs its url/i);
235
+ state = recordurl(state, { url: "https://example.com/a", stepid: "s1", now });
236
+ state = recordurl(state, { url: "https://example.com/a", stepid: "s1", now: now + 1 });
237
+ expect(state.urlhistory.length).toBe(1);
238
+ state = recordurl(state, { url: "https://example.com/a", stepid: "s2", now: now + 2 });
239
+ expect(state.urlhistory.length).toBe(2);
240
+ expect(() =>
241
+ recordenvironment(state, { stepid: "", environment: "pagecontext", origin: "https://example.com", now }),
242
+ ).toThrow(/step id/i);
243
+ state = recordenvironment(state, {
244
+ stepid: "s1",
245
+ environment: "offscreenworker",
246
+ origin: "https://example.com",
247
+ now,
248
+ });
249
+ expect(state.environments["s1"]).toBe("offscreenworker");
250
+ expect(state.lastprovenance?.environment).toBe("offscreenworker");
251
+ expect(() => recordturnaround(state, { stepid: "s1", milliseconds: -1, now })).toThrow(/non-negative/i);
252
+ expect(() => recordturnaround(state, { stepid: "s1", milliseconds: Number.NaN, now })).toThrow(/non-negative/i);
253
+ state = recordturnaround(state, { stepid: "s1", milliseconds: 0, now });
254
+ expect(state.turnarounds["s1"]).toBe(0);
255
+ });
256
+ });
257
+
258
+ describe("torture: run locks and concurrency refusal", () => {
259
+ it("refuses a second concurrent run of the same session while the same run renews", () => {
260
+ const first = acquirerunlock({ locks: [], sessionid: "s1", runid: "r1", holder: "executor", now });
261
+ expect(first.acquired).toBe(true);
262
+ const second = acquirerunlock({
263
+ locks: first.locks,
264
+ sessionid: "s1",
265
+ runid: "r2",
266
+ holder: "executor",
267
+ now: now + 1,
268
+ });
269
+ expect(second.acquired).toBe(false);
270
+ expect(second.reason).toMatch(/never carries two concurrent runs/i);
271
+ expect(second.locks).toBe(first.locks);
272
+ const renew = acquirerunlock({
273
+ locks: first.locks,
274
+ sessionid: "s1",
275
+ runid: "r1",
276
+ holder: "executor",
277
+ now: now + 2,
278
+ });
279
+ expect(renew.acquired).toBe(true);
280
+ expect(renew.locks.length).toBe(1);
281
+ });
282
+
283
+ it("expires the locks past their expiry naming their sessions and never at the boundary", () => {
284
+ const { locks } = acquirerunlock({
285
+ locks: [],
286
+ sessionid: "s1",
287
+ runid: "r1",
288
+ holder: "executor",
289
+ expiresat: now + 1_000,
290
+ now,
291
+ });
292
+ expect(expirerunlocks(locks, now).expired).toEqual([]);
293
+ expect(expirerunlocks(locks, now + 999).expired).toEqual([]);
294
+ expect(expirerunlocks(locks, now + 1_000).expired).toEqual([]);
295
+ expect(expirerunlocks(locks, now + 1_001).expired).toEqual(["s1"]);
296
+ expect(expirerunlocks(locks, now + 1_001).locks).toEqual([]);
297
+ const takenover = acquirerunlock({ locks, sessionid: "s1", runid: "r2", holder: "executor", now: now + 1_001 });
298
+ expect(takenover.acquired).toBe(true);
299
+ });
300
+
301
+ it("releases the run lock exactly for its own run", () => {
302
+ const { locks } = acquirerunlock({ locks: [], sessionid: "s1", runid: "r1", holder: "executor", now });
303
+ const released = releaserunlock({ locks, sessionid: "s1", runid: "r1", now });
304
+ expect(released.released).toBe(true);
305
+ expect(released.locks.length).toBe(0);
306
+ const missing = releaserunlock({ locks: released.locks, sessionid: "s1", runid: "other", now });
307
+ expect(missing.released).toBe(false);
308
+ expect(missing.reason).toMatch(/holds no lock/i);
309
+ });
310
+
311
+ it("serializes steps that share one tab across parallel branches", () => {
312
+ const order = serializesteps({
313
+ branches: [
314
+ {
315
+ branchid: "b1",
316
+ steps: [
317
+ { stepid: "a1", tabid: 7 },
318
+ { stepid: "a2", tabid: 8 },
319
+ ],
320
+ },
321
+ {
322
+ branchid: "b2",
323
+ steps: [
324
+ { stepid: "b1", tabid: 7 },
325
+ { stepid: "b2", tabid: 9 },
326
+ ],
327
+ },
328
+ ],
329
+ });
330
+ const tabseven = order.filter((entry) => entry.tabid === 7).map((entry) => entry.stepid);
331
+ expect(tabseven).toEqual(["a1", "b1"]);
332
+ const unique = new Set(order.map((entry) => entry.order));
333
+ expect(unique.size).toBe(order.length);
334
+ expect(order.length).toBe(4);
335
+ });
336
+ });
337
+
338
+ describe("torture: offline queue ordering and replay", () => {
339
+ it("queues only approved plans with monotonic sequence and drains in order", () => {
340
+ const queue = [
341
+ enqueueoffline([], { plan: plan({ id: "p1" }), sessionid: "s1", now }),
342
+ enqueueoffline(enqueueoffline([], { plan: plan({ id: "p1" }), sessionid: "s1", now }), {
343
+ plan: plan({ id: "p2" }),
344
+ sessionid: "s2",
345
+ now: now + 1,
346
+ }),
347
+ ];
348
+ expect(() => enqueueoffline(queue[0]!, { plan: plan({ id: "px", state: "draft" }), sessionid: "s1", now })).toThrow(
349
+ /only an approved plan/i,
350
+ );
351
+ const drained: string[] = [];
352
+ let rest = queue[1]!;
353
+ for (let index = 0; index < 3; index += 1) {
354
+ const next = dequeueoffline(rest);
355
+ if (next.task) drained.push(next.task.planid);
356
+ rest = next.queue;
357
+ }
358
+ expect(drained).toEqual(["p1", "p2"]);
359
+ expect(dequeueoffline(rest).task).toBeUndefined();
360
+ });
361
+
362
+ it("expires exactly the plans whose window passed while draining offline (the expiry tick is exclusive)", () => {
363
+ const early = enqueueoffline([], { plan: plan({ id: "p1", expiresat: now + 1_000 }), sessionid: "s1", now });
364
+ const late = enqueueoffline(early, { plan: plan({ id: "p2", expiresat: now + 60_000 }), sessionid: "s1", now });
365
+ const before = replayqueue(late, now + 999);
366
+ expect(before.expired.map((task) => task.planid)).toEqual([]);
367
+ expect(before.ready.map((task) => task.planid)).toEqual(["p1", "p2"]);
368
+ const atboundary = replayqueue(late, now + 1_000);
369
+ expect(atboundary.expired.map((task) => task.planid)).toEqual(["p1"]);
370
+ expect(atboundary.ready.map((task) => task.planid)).toEqual(["p2"]);
371
+ const after = replayqueue(late, now + 1_001);
372
+ expect(after.expired.map((task) => task.planid)).toEqual(["p1"]);
373
+ expect(after.ready.map((task) => task.planid)).toEqual(["p2"]);
374
+ });
375
+
376
+ it("keeps sequence order under interleaved enqueue bursts", () => {
377
+ let queue: ReturnType<typeof enqueueoffline> = [];
378
+ for (let index = 0; index < 50; index += 1) {
379
+ queue = enqueueoffline(queue, { plan: plan({ id: `p${index}` }), sessionid: "s1", now: now + index });
380
+ }
381
+ const order: number[] = [];
382
+ let rest = queue;
383
+ while (rest.length > 0) {
384
+ const next = dequeueoffline(rest);
385
+ if (next.task) order.push(next.task.sequence);
386
+ rest = next.queue;
387
+ }
388
+ expect(order).toEqual([...order].sort((one, two) => one - two));
389
+ expect(order.length).toBe(50);
390
+ });
391
+ });
392
+
393
+ describe("torture: checkpoints, digests and resume", () => {
394
+ it("refuses checkpoints without their ids and digest", () => {
395
+ expect(() => makecheckpoint({ runid: "", stepid: "s1", completed: [], digest: "d", now })).toThrow(/run id/i);
396
+ expect(() => makecheckpoint({ runid: "r1", stepid: "", completed: [], digest: "d", now })).toThrow(/step id/i);
397
+ expect(() => makecheckpoint({ runid: "r1", stepid: "s1", completed: [], digest: " ", now })).toThrow(
398
+ /page digest/i,
399
+ );
400
+ const checkpoint = makecheckpoint({ runid: "r1", stepid: "s3", completed: ["s1", "s2"], digest: "digest-a", now });
401
+ expect(checkpoint.completed).toEqual(["s1", "s2"]);
402
+ expect(checkpoint.completed).not.toBe(["s1", "s2"]);
403
+ });
404
+
405
+ it("validates only the checkpoint of its own run and unchanged digest", () => {
406
+ const checkpoint = makecheckpoint({ runid: "r1", stepid: "s3", completed: ["s1"], digest: "digest-a", now });
407
+ expect(checkpointisvalid({ checkpoint, runid: "r1", digest: "digest-a" })).toBe(true);
408
+ expect(checkpointisvalid({ checkpoint, runid: "r2", digest: "digest-a" })).toBe(false);
409
+ expect(checkpointisvalid({ checkpoint, runid: "r1", digest: "digest-b" })).toBe(false);
410
+ });
411
+
412
+ it("resumes from the first open step while a foreign checkpoint skips only named steps", () => {
413
+ const steps = ["s1", "s2", "s3", "s4"];
414
+ expect(resumecheckpoint({ checkpoint: undefined, steps })).toEqual(steps);
415
+ const checkpoint = makecheckpoint({ runid: "r1", stepid: "s2", completed: ["s1", "s2"], digest: "d", now });
416
+ expect(resumecheckpoint({ checkpoint, steps })).toEqual(["s3", "s4"]);
417
+ expect(resumecheckpoint({ checkpoint, steps: [] })).toEqual([]);
418
+ const foreign = makecheckpoint({ runid: "r2", stepid: "s9", completed: ["s1", "s9"], digest: "d", now });
419
+ expect(resumecheckpoint({ checkpoint: foreign, steps })).toEqual(["s2", "s3", "s4"]);
420
+ });
421
+
422
+ it("freezes progress on failure with the run id stamped", () => {
423
+ const moving = advancerun(run(), "running", now);
424
+ const failed = runfail({
425
+ run: moving,
426
+ progress: { planid: "p1", completedsteps: ["s1"], updatedat: now },
427
+ now: now + 1,
428
+ });
429
+ expect(failed.run.state).toBe("failed");
430
+ expect(failed.frozen?.runid).toBe("r1");
431
+ expect(failed.frozen?.updatedat).toBe(now + 1);
432
+ const bare = runfail({ run: moving, now: now + 2 });
433
+ expect(bare.frozen).toBeUndefined();
434
+ });
435
+
436
+ it("completes only a run whose plan finished every step", () => {
437
+ const planfixture = plan({ steps: [readstep("s1"), readstep("s2")] });
438
+ const moving = advancerun(run(), "running", now);
439
+ expect(() =>
440
+ runcomplete({
441
+ run: moving,
442
+ plan: planfixture,
443
+ progress: { planid: "p1", completedsteps: ["s1"], updatedat: now },
444
+ now,
445
+ }),
446
+ ).toThrow(/never completes/i);
447
+ const done = runcomplete({
448
+ run: moving,
449
+ plan: planfixture,
450
+ progress: { planid: "p1", completedsteps: ["s1", "s2"], updatedat: now },
451
+ now: now + 1,
452
+ });
453
+ expect(done.state).toBe("completed");
454
+ });
455
+
456
+ it("rebuilds a failed run from an expired or unapproved plan and a running one otherwise", () => {
457
+ expect(runfromplan(plan({ state: "draft" }), "s1", now).state).toBe("failed");
458
+ expect(runfromplan(plan({ expiresat: now }), "s1", now).state).toBe("failed");
459
+ expect(runfromplan(plan({ expiresat: now + 1 }), "s1", now).state).toBe("running");
460
+ expect(runfromplan(plan(), "s1", now).runid).toBe("p1");
461
+ });
462
+ });
463
+
464
+ describe("torture: heartbeats and the zombie check", () => {
465
+ it("stale heartbeat sits exactly past the window and never at it", () => {
466
+ const beat = runheartbeat({ runid: "r1", beat: 5, now });
467
+ expect(beat.beat).toBe(6);
468
+ expect(heartbeatisstale({ heartbeat: beat, now: now + 10_000, window: 10_000 })).toBe(false);
469
+ expect(heartbeatisstale({ heartbeat: beat, now: now + 10_001, window: 10_000 })).toBe(true);
470
+ expect(heartbeatisstale({ heartbeat: undefined, now, window: 1 })).toBe(true);
471
+ });
472
+
473
+ it("refuses blank run ids on heartbeats", () => {
474
+ expect(() => runheartbeat({ runid: " ", now })).toThrow(/run id/i);
475
+ expect(() => runheartbeat({ runid: "", now })).toThrow(/run id/i);
476
+ });
477
+
478
+ it("zombiecheck names only running runs with stale heartbeats", () => {
479
+ const runs = [
480
+ run({ runid: "alive", state: "running" }),
481
+ run({ runid: "zombie", state: "running" }),
482
+ run({ runid: "queued", state: "queued" }),
483
+ run({ runid: "done", state: "completed" }),
484
+ ];
485
+ const heartbeats: Record<string, heartbeatrecord> = {
486
+ alive: { runid: "alive", beat: 1, at: now - 5_000 },
487
+ zombie: { runid: "zombie", beat: 1, at: now - 50_000 },
488
+ queued: { runid: "queued", beat: 1, at: now - 50_000 },
489
+ done: { runid: "done", beat: 1, at: now - 50_000 },
490
+ };
491
+ const zombies = zombiecheck({ runs, heartbeats, now, window: 10_000 });
492
+ expect(zombies.map((entry) => entry.runid)).toEqual(["zombie"]);
493
+ });
494
+
495
+ it("reads the heartbeat window from settings with the roadmap default", () => {
496
+ expect(heartbeatwindowof(undefined)).toBe(60_000);
497
+ const settings: runsettings = {};
498
+ expect(heartbeatwindowof(settings)).toBe(60_000);
499
+ });
500
+ });
501
+
502
+ describe("torture: side effects and rollback compensations", () => {
503
+ it("compensates only the executed mutating steps while read steps carry none", () => {
504
+ const steps = [readstep("r1"), writestep("w1"), writestep("w2"), writestep("w3")];
505
+ const executed = ["r1", "w1", "w3"];
506
+ const effects = sideeffects(steps, executed);
507
+ expect(effects.map((step) => step.id)).toEqual(["w1", "w3"]);
508
+ const items = rollbackrun({ runid: "r1", steps, executedstepids: executed, origin: "https://example.com", now });
509
+ expect(items.map((item) => item.stepid)).toEqual(["w1", "w3"]);
510
+ expect(items.every((item) => item.compensation.length > 0)).toBe(true);
511
+ expect(items.every((item) => item.origin === "https://example.com")).toBe(true);
512
+ expect(rollbackrun({ runid: "r1", steps, executedstepids: [], origin: "https://example.com", now })).toEqual([]);
513
+ });
514
+
515
+ it("reports the fallback compensation for an unknown mutating kind", () => {
516
+ const alien = { id: "x1", kind: "dimensionhop", summary: "unknown", risk: "sensitive" } as unknown as toolstep;
517
+ const items = rollbackrun({
518
+ runid: "r1",
519
+ steps: [alien],
520
+ executedstepids: ["x1"],
521
+ origin: "https://example.com",
522
+ now,
523
+ });
524
+ expect(items[0]?.compensation).toMatch(/dimensionhop step stays executed/i);
525
+ });
526
+
527
+ it("cancel pairs the rollback choice with compensations only under the explicit user choice", () => {
528
+ const steps = [writestep("w1"), readstep("r1")];
529
+ const moving = advancerun(run(), "running", now);
530
+ const none = cancelrollback({
531
+ run: moving,
532
+ steps,
533
+ executedstepids: ["w1", "r1"],
534
+ origin: "https://example.com",
535
+ choice: "none",
536
+ now: now + 1,
537
+ });
538
+ expect(none.run.state).toBe("cancelled");
539
+ expect(none.compensations).toEqual([]);
540
+ expect(none.reason).toMatch(/without a rollback/i);
541
+ const fresh = advancerun(run({ runid: "r2" }), "running", now);
542
+ const rollback = cancelrollback({
543
+ run: fresh,
544
+ steps,
545
+ executedstepids: ["w1", "r1"],
546
+ origin: "https://example.com",
547
+ choice: "rollback",
548
+ now: now + 1,
549
+ });
550
+ expect(rollback.run.state).toBe("rolledback");
551
+ expect(rollback.compensations.map((item) => item.stepid)).toEqual(["w1"]);
552
+ expect(rollback.reason).toMatch(/explicit user choice/i);
553
+ expect(() =>
554
+ cancelrollback({
555
+ run: rollback.run,
556
+ steps,
557
+ executedstepids: ["w1"],
558
+ origin: "https://example.com",
559
+ choice: "none",
560
+ now: now + 2,
561
+ }),
562
+ ).toThrow(/cannot move/i);
563
+ expect(() =>
564
+ cancelrollback({
565
+ run: none.run,
566
+ steps,
567
+ executedstepids: [],
568
+ origin: "https://example.com",
569
+ choice: "none",
570
+ now: now + 2,
571
+ }),
572
+ ).toThrow(/cannot move/i);
573
+ });
574
+ });
575
+
576
+ describe("torture: budget alerts and timeout cancels", () => {
577
+ it("budget alerts fire at their reached severity with critical pausing and warnings informational", () => {
578
+ const atwarning = budgetalertstate({
579
+ budget: runbudgetof({ runid: "r1", steps: 80, stepbudget: 100, pressure: 0.5, now }),
580
+ thresholds: { warning: 0.8, critical: 0.95 },
581
+ });
582
+ expect(atwarning.map((alert) => alert.level)).toEqual(["warning"]);
583
+ expect(runpausesforalerts(atwarning)).toBe(false);
584
+ const atcritical = budgetalertstate({
585
+ budget: runbudgetof({ runid: "r1", steps: 100, stepbudget: 100, pressure: 0.5, now }),
586
+ thresholds: { warning: 0.8, critical: 0.95 },
587
+ });
588
+ expect(atcritical.map((alert) => alert.level)).toEqual(["critical"]);
589
+ expect(runpausesforalerts(atcritical)).toBe(true);
590
+ const over = budgetalertstate({
591
+ budget: runbudgetof({ runid: "r1", steps: 101, stepbudget: 100, pressure: 0.5, now }),
592
+ thresholds: { warning: 0.5, critical: 0.5 },
593
+ });
594
+ expect(runpausesforalerts(over)).toBe(true);
595
+ const under = budgetalertstate({
596
+ budget: runbudgetof({ runid: "r1", steps: 10, stepbudget: 100, pressure: 0.5, now }),
597
+ thresholds: { warning: 0.8, critical: 0.95 },
598
+ });
599
+ expect(under).toEqual([]);
600
+ const nobudget = budgetalertstate({
601
+ budget: runbudgetof({ runid: "r1", steps: 10, pressure: 0.5, now }),
602
+ thresholds: { warning: 0.5, critical: 0.5 },
603
+ });
604
+ expect(nobudget).toEqual([]);
605
+ });
606
+
607
+ it("timeout cancels at exactly the bound and one past it while memory pressure alerts beside the steps", () => {
608
+ expect(timeoutcanceldecision({ elapsed: 999, bound: 1_000 }).abort).toBe(false);
609
+ expect(timeoutcanceldecision({ elapsed: 1_000, bound: 1_000 }).abort).toBe(false);
610
+ expect(timeoutcanceldecision({ elapsed: 1_001, bound: 1_000 }).abort).toBe(true);
611
+ expect(timeoutcanceldecision({ elapsed: 0, bound: 0 }).abort).toBe(false);
612
+ expect(timeoutcanceldecision({ elapsed: -5, bound: 1_000 }).abort).toBe(false);
613
+ const unbound = timeoutcanceldecision({ elapsed: 10_000_000 });
614
+ expect(unbound.abort).toBe(false);
615
+ expect(unbound.reason).toMatch(/no timeout bound/i);
616
+ const gap = timeoutcanceldecision({ elapsed: Number.NaN, bound: 1_000 });
617
+ expect(gap.abort).toBe(false);
618
+ expect(gap.reason).toMatch(/no finite elapsed/i);
619
+ const memory = budgetalertstate({
620
+ budget: runbudgetof({ runid: "r1", steps: 1, pressure: 1, memorybudget: 1, now }),
621
+ thresholds: { warning: 0.5, critical: 0.9 },
622
+ });
623
+ expect(memory.map((alert) => alert.kind)).toEqual(["memory"]);
624
+ expect(runpausesforalerts(memory)).toBe(true);
625
+ });
626
+ });