@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,829 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ activetimelineanchor,
4
+ appendlogstreamevent,
5
+ auditexcerptof,
6
+ diffpreviewof,
7
+ filterlogstream,
8
+ formatdiagnostics,
9
+ lintplanfile,
10
+ livebufferof,
11
+ livebufferof as _unused,
12
+ loglevelof,
13
+ logstreameventof,
14
+ logstreamgenesis,
15
+ loopkinds,
16
+ maskverdictsof,
17
+ parseplanfile,
18
+ plancardgroups,
19
+ plancardsof,
20
+ planfilestepfields,
21
+ planlintexitcode,
22
+ planlintsummary,
23
+ planoriginprofile,
24
+ plansensitiveclasses,
25
+ plansteprisk,
26
+ resolutionhistoryafter,
27
+ resolutionlogeventof,
28
+ retrykinds,
29
+ rulesetcachekey,
30
+ stepresolutionof,
31
+ stepstimelinenodes,
32
+ verifylogstream,
33
+ } from "../plan.js";
34
+ import type {
35
+ agentplan,
36
+ classconsent,
37
+ correctionentry,
38
+ planfile,
39
+ portableruleset,
40
+ toolstep,
41
+ uisurface,
42
+ } from "../types.js";
43
+
44
+ const now = 1_800_000_000_000;
45
+
46
+ /** Builds one minimal plan file fixture that lints clean against the empty ruleset. */
47
+ function planfile(over: Partial<planfile> = {}): planfile {
48
+ return {
49
+ version: "1",
50
+ goal: "Scrape the pricing page",
51
+ origin: "https://example.com",
52
+ steps: [
53
+ { id: "s1", kind: "navigate", label: "open the page" },
54
+ { id: "s2", kind: "readtext", label: "read the heading" },
55
+ ],
56
+ ...over,
57
+ };
58
+ }
59
+
60
+ /** Builds one portable ruleset fixture with the named rule ids active. */
61
+ function ruleset(ids: string[] = []): portableruleset {
62
+ return { version: "1", compiledat: now, rules: ids.map((id) => ({ id, family: "planlint", validates: id })) };
63
+ }
64
+
65
+ /** Builds one approved agentplan fixture. */
66
+ function approvedplan(over: Partial<agentplan> = {}): agentplan {
67
+ return {
68
+ id: "plan1",
69
+ objective: "torture the plan module",
70
+ origin: "https://example.com",
71
+ steps: [
72
+ { id: "s1", kind: "navigate", value: "https://example.com", summary: "open the page", risk: "sensitive" },
73
+ { id: "s2", kind: "readtext", target: "h1", summary: "read the heading", risk: "read" },
74
+ ],
75
+ createdat: now - 1000,
76
+ expiresat: now + 600_000,
77
+ state: "approved",
78
+ ...over,
79
+ };
80
+ }
81
+
82
+ describe("torture: plan file parse under schemastrict", () => {
83
+ it("parses one minimal plan file with the documented step fields and grants", () => {
84
+ const parsed = parseplanfile({
85
+ version: "1",
86
+ goal: "go",
87
+ origin: "https://example.com",
88
+ steps: [{ id: "s1", kind: "navigate", label: "open" }],
89
+ grants: ["navigate"],
90
+ denials: ["click"],
91
+ });
92
+ expect(parsed.version).toBe("1");
93
+ expect(parsed.goal).toBe("go");
94
+ expect(parsed.origin).toBe("https://example.com");
95
+ expect(parsed.steps).toHaveLength(1);
96
+ expect(parsed.grants).toEqual(["navigate"]);
97
+ expect(parsed.denials).toEqual(["click"]);
98
+ });
99
+
100
+ it("refuses non object, null and array payloads", () => {
101
+ expect(() => parseplanfile("not an object")).toThrow(/json object/i);
102
+ expect(() => parseplanfile(null)).toThrow(/json object/i);
103
+ expect(() => parseplanfile([])).toThrow(/json object/i);
104
+ expect(() => parseplanfile(42)).toThrow(/json object/i);
105
+ expect(() => parseplanfile(undefined)).toThrow(/json object/i);
106
+ });
107
+
108
+ it("refuses unknown root and step fields under schemastrict", () => {
109
+ expect(() =>
110
+ parseplanfile({
111
+ version: "1",
112
+ goal: "g",
113
+ origin: "https://example.com",
114
+ steps: [{ id: "s1", kind: "navigate", label: "open" }],
115
+ ghost: "haunt",
116
+ }),
117
+ ).toThrow(/unknown field/i);
118
+ expect(() =>
119
+ parseplanfile({
120
+ version: "1",
121
+ goal: "g",
122
+ origin: "https://example.com",
123
+ steps: [{ id: "s1", kind: "navigate", label: "open", ghost: "haunt" }],
124
+ }),
125
+ ).toThrow(/unknown field/i);
126
+ });
127
+
128
+ it("refuses the non https origin and the empty step list", () => {
129
+ expect(() =>
130
+ parseplanfile({
131
+ version: "1",
132
+ goal: "g",
133
+ origin: "http://example.com",
134
+ steps: [{ id: "s1", kind: "navigate", label: "open" }],
135
+ }),
136
+ ).toThrow(/HTTPS/i);
137
+ expect(() => parseplanfile({ version: "1", goal: "g", origin: "https://example.com", steps: [] })).toThrow(
138
+ /at least one step/i,
139
+ );
140
+ });
141
+
142
+ it("refuses blank ids, kinds, labels and accepts the optional target, value and options", () => {
143
+ expect(() =>
144
+ parseplanfile({
145
+ version: "1",
146
+ goal: "g",
147
+ origin: "https://example.com",
148
+ steps: [{ id: " ", kind: "navigate", label: "open" }],
149
+ }),
150
+ ).toThrow(/non-empty string/i);
151
+ expect(() =>
152
+ parseplanfile({
153
+ version: "1",
154
+ goal: "g",
155
+ origin: "https://example.com",
156
+ steps: [{ id: "s1", kind: "", label: "open" }],
157
+ }),
158
+ ).toThrow(/non-empty string/i);
159
+ expect(() =>
160
+ parseplanfile({
161
+ version: "1",
162
+ goal: "g",
163
+ origin: "https://example.com",
164
+ steps: [{ id: "s1", kind: "navigate", label: "" }],
165
+ }),
166
+ ).toThrow(/non-empty string/i);
167
+ const parsed = parseplanfile({
168
+ version: "1",
169
+ goal: "g",
170
+ origin: "https://example.com",
171
+ steps: [{ id: "s1", kind: "type", label: "open", target: "#user", value: "alice", options: "{}" }],
172
+ });
173
+ expect(parsed.steps[0]).toMatchObject({
174
+ id: "s1",
175
+ kind: "type",
176
+ label: "open",
177
+ target: "#user",
178
+ value: "alice",
179
+ options: "{}",
180
+ });
181
+ });
182
+
183
+ it("refuses the non integer and non positive loop bound and retry attempts", () => {
184
+ expect(() =>
185
+ parseplanfile({
186
+ version: "1",
187
+ goal: "g",
188
+ origin: "https://example.com",
189
+ steps: [{ id: "s1", kind: "navigate", label: "open", bound: 0 }],
190
+ }),
191
+ ).toThrow(/positive integer/i);
192
+ expect(() =>
193
+ parseplanfile({
194
+ version: "1",
195
+ goal: "g",
196
+ origin: "https://example.com",
197
+ steps: [{ id: "s1", kind: "navigate", label: "open", bound: 1.5 }],
198
+ }),
199
+ ).toThrow(/positive integer/i);
200
+ expect(() =>
201
+ parseplanfile({
202
+ version: "1",
203
+ goal: "g",
204
+ origin: "https://example.com",
205
+ steps: [{ id: "s1", kind: "navigate", label: "open", attempts: -1 }],
206
+ }),
207
+ ).toThrow(/positive integer/i);
208
+ });
209
+
210
+ it("accepts the boolean gate declaration and refuses the non boolean one", () => {
211
+ const parsed = parseplanfile({
212
+ version: "1",
213
+ goal: "g",
214
+ origin: "https://example.com",
215
+ steps: [{ id: "s1", kind: "click", label: "open", gate: true }],
216
+ });
217
+ expect(parsed.steps[0]?.gate).toBe(true);
218
+ expect(() =>
219
+ parseplanfile({
220
+ version: "1",
221
+ goal: "g",
222
+ origin: "https://example.com",
223
+ steps: [{ id: "s1", kind: "click", label: "open", gate: "yes" }],
224
+ }),
225
+ ).toThrow(/not a boolean/i);
226
+ });
227
+
228
+ it("refuses the grant list with blank or non string entries", () => {
229
+ expect(() =>
230
+ parseplanfile({
231
+ version: "1",
232
+ goal: "g",
233
+ origin: "https://example.com",
234
+ steps: [{ id: "s1", kind: "navigate", label: "open" }],
235
+ grants: [""],
236
+ }),
237
+ ).toThrow(/non-empty action kind/i);
238
+ expect(() =>
239
+ parseplanfile({
240
+ version: "1",
241
+ goal: "g",
242
+ origin: "https://example.com",
243
+ steps: [{ id: "s1", kind: "navigate", label: "open" }],
244
+ grants: ["navigate", 5] as never,
245
+ }),
246
+ ).toThrow(/non-empty action kind/i);
247
+ });
248
+
249
+ it("preserves unicode, cjk and emoji ids and goals without truncation", () => {
250
+ const parsed = parseplanfile({
251
+ version: "1",
252
+ goal: "完成调查 🚀",
253
+ origin: "https://example.com",
254
+ steps: [{ id: "步1", kind: "navigate", label: "打开页面" }],
255
+ });
256
+ expect(parsed.goal).toBe("完成调查 🚀");
257
+ expect(parsed.steps[0]?.id).toBe("步1");
258
+ });
259
+
260
+ it("lists the documented step fields in the reviewed order", () => {
261
+ expect([...planfilestepfields]).toEqual([
262
+ "id",
263
+ "kind",
264
+ "label",
265
+ "target",
266
+ "value",
267
+ "options",
268
+ "gate",
269
+ "bound",
270
+ "attempts",
271
+ ]);
272
+ });
273
+ });
274
+
275
+ describe("torture: plan lint diagnostics and exit code", () => {
276
+ it("raises the loopbound warn for every unbounded loop and the retrybound warn for the missing attempts", () => {
277
+ const file = planfile({
278
+ steps: [
279
+ { id: "s1", kind: "loop", label: "loop the rows" },
280
+ { id: "s2", kind: "retryaction", label: "retry the click" },
281
+ { id: "s3", kind: "navigate", label: "open" },
282
+ ],
283
+ });
284
+ const diagnostics = lintplanfile({
285
+ file,
286
+ ruleset: ruleset(["loopbound", "retrybound"]),
287
+ capabilities: ["navigate"],
288
+ now,
289
+ });
290
+ expect(diagnostics.filter((d) => d.code === "plan.control.loopbound")).toHaveLength(1);
291
+ expect(diagnostics.filter((d) => d.code === "plan.control.retrybound")).toHaveLength(1);
292
+ expect(diagnostics.every((d) => d.severity === "warn")).toBe(true);
293
+ expect(planlintexitcode(diagnostics)).toBe(0);
294
+ });
295
+
296
+ it("raises the static selector info for templated targets and the goal and origin errors at the end", () => {
297
+ const file = planfile({
298
+ goal: "",
299
+ origin: "",
300
+ steps: [{ id: "s1", kind: "readtext", label: "open", target: "{{row}}" }],
301
+ });
302
+ const diagnostics = lintplanfile({ file, ruleset: ruleset(["staticselector"]), capabilities: ["readtext"], now });
303
+ expect(diagnostics.some((d) => d.code === "plan.selector.static")).toBe(true);
304
+ expect(diagnostics.some((d) => d.code === "plan.goal.empty")).toBe(true);
305
+ expect(diagnostics.some((d) => d.code === "plan.origin.empty")).toBe(true);
306
+ expect(planlintexitcode(diagnostics)).toBe(1);
307
+ });
308
+
309
+ it("reports the diagnostic counts and the exit code in the summary", () => {
310
+ const file = planfile({ goal: "", origin: "", steps: [{ id: "s1", kind: "loop", label: "loop" }] });
311
+ const diagnostics = lintplanfile({ file, ruleset: ruleset(["loopbound"]), capabilities: ["navigate"], now });
312
+ const summary = planlintsummary(diagnostics);
313
+ expect(summary.warn).toBe(1);
314
+ expect(summary.error).toBe(2);
315
+ expect(summary.exitcode).toBe(1);
316
+ expect(summary.info).toBe(0);
317
+ });
318
+
319
+ it("formats the diagnostics for human and json output", () => {
320
+ const diagnostics = [{ code: "plan.test", path: "steps[0]", severity: "error" as const, message: "boom" }];
321
+ expect(formatdiagnostics(diagnostics, "human")).toBe("ERROR plan.test at steps[0]: boom");
322
+ expect(JSON.parse(formatdiagnostics(diagnostics, "json"))).toHaveLength(1);
323
+ expect(formatdiagnostics([], "human")).toBe("No planlint diagnostics.");
324
+ });
325
+
326
+ it("computes the deterministic ruleset cache key from the version and the rule ids", () => {
327
+ expect(
328
+ rulesetcachekey({
329
+ version: "2",
330
+ compiledat: now,
331
+ rules: [
332
+ { id: "loopbound", family: "planlint", validates: "loopbound" },
333
+ { id: "retrybound", family: "planlint", validates: "retrybound" },
334
+ ],
335
+ }),
336
+ ).toBe("planlint-2-loopbound+retrybound");
337
+ });
338
+ });
339
+
340
+ describe("torture: plan step risk and sensitive class derivation", () => {
341
+ it("reads the derived risk of one step against the policy grading", () => {
342
+ expect(plansteprisk({ id: "s1", kind: "readtext", label: "read" })).toBe("read");
343
+ expect(plansteprisk({ id: "s1", kind: "navigate", label: "open" })).toBe("sensitive");
344
+ expect(plansteprisk({ id: "s1", kind: "teleport" as never, label: "ghost" })).toBe("sensitive");
345
+ });
346
+
347
+ it("lists the sensitive classes the plan needs consent for", () => {
348
+ const file = planfile({
349
+ steps: [
350
+ { id: "s1", kind: "navigate", label: "open" },
351
+ { id: "s2", kind: "type", label: "type", value: "alice" },
352
+ ],
353
+ });
354
+ const classes = plansensitiveclasses(file);
355
+ expect(Array.isArray(classes)).toBe(true);
356
+ });
357
+
358
+ it("builds the originprofile of one plan file with the declared grants and denials", () => {
359
+ const profile = planoriginprofile(planfile({ grants: ["navigate"], denials: ["click"] }), now);
360
+ expect(profile.origin).toBe("https://example.com");
361
+ expect(profile.grants).toEqual(["navigate"]);
362
+ expect(profile.denials).toEqual(["click"]);
363
+ });
364
+
365
+ it("reviews the documented loop kinds and retry kinds of the linter", () => {
366
+ expect([...loopkinds]).toEqual(["loop", "whileloop", "repeatuntil"]);
367
+ expect([...retrykinds]).toEqual(["retryaction", "trycatch"]);
368
+ });
369
+ });
370
+
371
+ describe("torture: plan review plancards and groups", () => {
372
+ it("builds one plancard per proposed step with the matching corrections", () => {
373
+ const corrections: correctionentry[] = [
374
+ {
375
+ id: "c1",
376
+ origin: "https://example.com",
377
+ kind: "navigate",
378
+ stepid: "s1",
379
+ original: "open",
380
+ corrected: "open the page",
381
+ reason: "made the summary clearer",
382
+ source: "edited",
383
+ at: now,
384
+ },
385
+ ];
386
+ const cards = plancardsof({ plan: approvedplan(), corrections });
387
+ expect(cards).toHaveLength(2);
388
+ expect(cards[0]).toMatchObject({ stepid: "s1", kind: "navigate", risk: "sensitive", editable: false });
389
+ expect(cards[0]?.corrections).toHaveLength(1);
390
+ expect(cards[0]?.corrections[0]?.id).toBe("c1");
391
+ });
392
+
393
+ it("marks the cards editable when the plan stays pending", () => {
394
+ const cards = plancardsof({ plan: approvedplan({ state: "pending" }), corrections: [] });
395
+ expect(cards.every((card) => card.editable)).toBe(true);
396
+ });
397
+
398
+ it("groups the cards by risk class with the sensitive group expanded by default", () => {
399
+ const cards = plancardsof({ plan: approvedplan(), corrections: [] });
400
+ const groups = plancardgroups(cards);
401
+ expect(groups.map((group) => group.risk)).toEqual(["sensitive", "read"]);
402
+ expect(groups[0]?.expanded).toBe(true);
403
+ expect(groups[1]?.expanded).toBe(false);
404
+ expect(plancardgroups([])).toEqual([]);
405
+ });
406
+
407
+ it("marks the read group folded and the sensitive group expanded for the full palette", () => {
408
+ const cards = [
409
+ {
410
+ stepid: "s1",
411
+ kind: "readtext",
412
+ risk: "read",
413
+ environment: "pagecontext",
414
+ options: "",
415
+ summary: "read",
416
+ corrections: [],
417
+ editable: false,
418
+ },
419
+ {
420
+ stepid: "s2",
421
+ kind: "click",
422
+ risk: "sensitive",
423
+ environment: "pagecontext",
424
+ options: "",
425
+ summary: "click",
426
+ corrections: [],
427
+ editable: false,
428
+ },
429
+ {
430
+ stepid: "s3",
431
+ kind: "hover",
432
+ risk: "interaction",
433
+ environment: "pagecontext",
434
+ options: "",
435
+ summary: "hover",
436
+ corrections: [],
437
+ editable: false,
438
+ },
439
+ ] as never;
440
+ const groups = plancardgroups(cards);
441
+ expect(groups.map((group) => group.risk)).toEqual(["sensitive", "interaction", "read"]);
442
+ expect(groups.every((group) => group.cards.length === 1)).toBe(true);
443
+ });
444
+ });
445
+
446
+ describe("torture: plan stepapprove resolution and immutable log event", () => {
447
+ const surface: uisurface = "sidepanel";
448
+
449
+ it("builds the approve, reject and edit resolutions with the edited shape", () => {
450
+ const approve = stepresolutionof({
451
+ stepid: "s1",
452
+ planid: "p1",
453
+ origin: "https://example.com",
454
+ resolution: "approve",
455
+ surface,
456
+ at: now,
457
+ });
458
+ expect(approve).toMatchObject({ stepid: "s1", resolution: "approve" });
459
+ expect(approve.edited).toBeUndefined();
460
+ const edit = stepresolutionof({
461
+ stepid: "s1",
462
+ planid: "p1",
463
+ origin: "https://example.com",
464
+ resolution: "edit",
465
+ surface,
466
+ edited: "the new step shape",
467
+ at: now,
468
+ });
469
+ expect(edit.edited).toBe("the new step shape");
470
+ });
471
+
472
+ it("refuses the blank stepid and the edited resolution without the edited shape", () => {
473
+ expect(() =>
474
+ stepresolutionof({
475
+ stepid: "",
476
+ planid: "p1",
477
+ origin: "https://example.com",
478
+ resolution: "approve",
479
+ surface,
480
+ at: now,
481
+ }),
482
+ ).toThrow(/step/i);
483
+ expect(() =>
484
+ stepresolutionof({
485
+ stepid: "s1",
486
+ planid: "p1",
487
+ origin: "https://example.com",
488
+ resolution: "edit",
489
+ surface,
490
+ edited: " ",
491
+ at: now,
492
+ }),
493
+ ).toThrow(/corrected step/i);
494
+ });
495
+
496
+ it("builds the immutable log event summary for the approve, reject and edit resolutions", () => {
497
+ const approve = stepresolutionof({
498
+ stepid: "s1",
499
+ planid: "p1",
500
+ origin: "https://example.com",
501
+ resolution: "approve",
502
+ surface,
503
+ at: now,
504
+ });
505
+ expect(resolutionlogeventof(approve)).toMatchObject({ kind: "review", stepid: "s1" });
506
+ expect(resolutionlogeventof(approve).summary).toMatch(/approved/i);
507
+ const reject = stepresolutionof({
508
+ stepid: "s1",
509
+ planid: "p1",
510
+ origin: "https://example.com",
511
+ resolution: "reject",
512
+ surface,
513
+ at: now,
514
+ });
515
+ expect(resolutionlogeventof(reject).summary).toMatch(/rejected/i);
516
+ const edit = stepresolutionof({
517
+ stepid: "s1",
518
+ planid: "p1",
519
+ origin: "https://example.com",
520
+ resolution: "edit",
521
+ surface,
522
+ edited: "x",
523
+ at: now,
524
+ });
525
+ expect(resolutionlogeventof(edit).summary).toMatch(/edited/i);
526
+ });
527
+
528
+ it("appends the resolution to the per origin history with the newest first", () => {
529
+ const first = stepresolutionof({
530
+ stepid: "s1",
531
+ planid: "p1",
532
+ origin: "https://example.com",
533
+ resolution: "approve",
534
+ surface,
535
+ at: now,
536
+ });
537
+ const second = stepresolutionof({
538
+ stepid: "s2",
539
+ planid: "p1",
540
+ origin: "https://example.com",
541
+ resolution: "approve",
542
+ surface,
543
+ at: now + 1,
544
+ });
545
+ const history = resolutionhistoryafter(resolutionhistoryafter([], first), second);
546
+ expect(history).toHaveLength(2);
547
+ expect(history[0]?.stepid).toBe("s2");
548
+ });
549
+ });
550
+
551
+ describe("torture: plan diff preview and mask verdicts", () => {
552
+ it("compares the before and after states and reports the added, changed and removed fields", () => {
553
+ const diff = diffpreviewof({
554
+ stepid: "s1",
555
+ before: { name: "alice", age: "30", removed: "x" },
556
+ after: { name: "alice2", age: "30", added: "y" },
557
+ provenance: "offscreenworker",
558
+ });
559
+ expect(diff.changes.map((c) => c.field)).toEqual(["name", "removed", "added"]);
560
+ expect(diff.changes.find((c) => c.field === "name")).toMatchObject({
561
+ kind: "changed",
562
+ before: "alice",
563
+ after: "alice2",
564
+ });
565
+ expect(diff.changes.find((c) => c.field === "removed")).toMatchObject({ kind: "removed", before: "x" });
566
+ expect(diff.changes.find((c) => c.field === "added")).toMatchObject({ kind: "added", after: "y" });
567
+ expect(diff.provenance).toBe("offscreenworker");
568
+ });
569
+
570
+ it("returns no changes when the states match", () => {
571
+ const diff = diffpreviewof({ stepid: "s1", before: { a: "1" }, after: { a: "1" }, provenance: "inline" });
572
+ expect(diff.changes).toEqual([]);
573
+ });
574
+
575
+ it("derives the mask verdicts of the sensitive fields", () => {
576
+ const verdicts = maskverdictsof({ name: "alice", password: "hunter2", token: "sk-1234567890" }, [
577
+ "password",
578
+ "token",
579
+ ]);
580
+ expect(Object.keys(verdicts)).toEqual(["password", "token"]);
581
+ expect(verdicts.password).toMatch(/masked/i);
582
+ expect(verdicts.name).toBeUndefined();
583
+ });
584
+
585
+ it("carries the mask verdicts through the diff preview payload", () => {
586
+ const verdicts = maskverdictsof({ password: "hunter2" }, ["password"]);
587
+ const diff = diffpreviewof({
588
+ stepid: "s1",
589
+ before: {},
590
+ after: { password: "hunter2" },
591
+ maskverdicts: verdicts,
592
+ provenance: "offscreenworker",
593
+ });
594
+ expect(diff.maskverdicts).toBe(verdicts);
595
+ });
596
+ });
597
+
598
+ describe("torture: plan steps timeline nodes from progress", () => {
599
+ it("derives the done, running and pending statuses from the plan state and progress", () => {
600
+ const nodes = stepstimelinenodes({ plan: approvedplan(), now });
601
+ expect(nodes.map((node) => node.status)).toEqual(["running", "pending"]);
602
+ expect(nodes.find((node) => node.active)?.stepid).toBe("s1");
603
+ expect(activetimelineanchor(nodes)).toBe("#step-s1");
604
+ });
605
+
606
+ it("marks the cancelled and expired plans as halted with no active node", () => {
607
+ const nodes = stepstimelinenodes({ plan: approvedplan({ state: "cancelled" }), now });
608
+ expect(nodes.every((node) => node.status === "halted")).toBe(true);
609
+ expect(activetimelineanchor(nodes)).toBeUndefined();
610
+ const expired = stepstimelinenodes({ plan: approvedplan({ state: "expired" }), now });
611
+ expect(expired.every((node) => node.status === "halted")).toBe(true);
612
+ });
613
+
614
+ it("reports the done status from the completedsteps when no outcome carries one", () => {
615
+ const nodes = stepstimelinenodes({
616
+ plan: approvedplan(),
617
+ progress: { planid: "plan1", completedsteps: ["s1", "s2"], updatedat: now },
618
+ now,
619
+ });
620
+ expect(nodes.map((node) => node.status)).toEqual(["done", "done"]);
621
+ });
622
+
623
+ it("reads the failed outcome and the waiting gate wait status", () => {
624
+ const nodes = stepstimelinenodes({
625
+ plan: approvedplan(),
626
+ progress: {
627
+ planid: "plan1",
628
+ completedsteps: [],
629
+ outcomes: [{ stepid: "s1", ok: false, summary: "boom", at: now }],
630
+ gatewaits: { s2: { gateid: "s2", kind: "confirm", openedat: now, resolvedat: now, waitedms: 1 } },
631
+ updatedat: now,
632
+ },
633
+ now,
634
+ });
635
+ expect(nodes[0]?.status).toBe("failed");
636
+ expect(nodes[1]?.status).toBe("waiting");
637
+ expect(nodes[1]?.active).toBe(false);
638
+ });
639
+
640
+ it("carries the duration, the environment and the result summary from the progress", () => {
641
+ const nodes = stepstimelinenodes({
642
+ plan: approvedplan(),
643
+ progress: {
644
+ planid: "plan1",
645
+ completedsteps: ["s1"],
646
+ outcomes: [{ stepid: "s1", ok: true, summary: "done", at: now }],
647
+ turnarounds: { s1: 42 },
648
+ environments: { s1: "offscreenworker" },
649
+ updatedat: now,
650
+ },
651
+ now,
652
+ });
653
+ expect(nodes[0]).toMatchObject({ durationms: 42, environment: "offscreenworker", resultsummary: "done" });
654
+ });
655
+ });
656
+
657
+ describe("torture: plan logstream hash chain integrity", () => {
658
+ it("chains the events under the genesis hash and verifies the chain link by link", async () => {
659
+ const first = await logstreameventof({
660
+ level: "info",
661
+ source: "console",
662
+ origin: "https://example.com",
663
+ summary: "first event",
664
+ masked: false,
665
+ maskverdict: "",
666
+ previous: logstreamgenesis,
667
+ at: now,
668
+ });
669
+ const second = await logstreameventof({
670
+ level: "warn",
671
+ source: "error",
672
+ origin: "https://example.com",
673
+ summary: "second event",
674
+ stepid: "s1",
675
+ masked: true,
676
+ maskverdict: "the password stays masked",
677
+ previous: first.hash.current,
678
+ at: now + 1,
679
+ });
680
+ const events = appendlogstreamevent(appendlogstreamevent([], first), second);
681
+ const verified = await verifylogstream(events);
682
+ expect(verified.valid).toBe(true);
683
+ });
684
+
685
+ it("refuses the empty summary event", async () => {
686
+ await expect(
687
+ logstreameventof({
688
+ level: "info",
689
+ source: "console",
690
+ origin: "https://example.com",
691
+ summary: " ",
692
+ masked: false,
693
+ maskverdict: "",
694
+ previous: logstreamgenesis,
695
+ at: now,
696
+ }),
697
+ ).rejects.toThrow(/summary/i);
698
+ });
699
+
700
+ it("detects the broken link and the tampered hash", async () => {
701
+ const first = await logstreameventof({
702
+ level: "info",
703
+ source: "console",
704
+ origin: "https://example.com",
705
+ summary: "first",
706
+ masked: false,
707
+ maskverdict: "",
708
+ previous: logstreamgenesis,
709
+ at: now,
710
+ });
711
+ const broken = await logstreameventof({
712
+ level: "info",
713
+ source: "console",
714
+ origin: "https://example.com",
715
+ summary: "second",
716
+ masked: false,
717
+ maskverdict: "",
718
+ previous: "0".repeat(64),
719
+ at: now + 1,
720
+ });
721
+ const events = appendlogstreamevent(appendlogstreamevent([], first), broken);
722
+ const verified = await verifylogstream(events);
723
+ expect(verified.valid).toBe(false);
724
+ expect(verified.brokenat).toBe(1);
725
+ const tampered = { ...broken, summary: "tampered summary that changes the hash" };
726
+ const tamperedEvents = appendlogstreamevent([first], tampered);
727
+ const tamperedVerified = await verifylogstream(tamperedEvents);
728
+ expect(tamperedVerified.valid).toBe(false);
729
+ });
730
+
731
+ it("filters the events by level, origin and step ref and the absent filter keeps everything", () => {
732
+ const events = [
733
+ {
734
+ id: "e1",
735
+ level: "info",
736
+ source: "console",
737
+ origin: "https://example.com",
738
+ summary: "a",
739
+ masked: false,
740
+ maskverdict: "",
741
+ hash: { previous: "x", current: "y" },
742
+ at: now,
743
+ },
744
+ {
745
+ id: "e2",
746
+ level: "warn",
747
+ source: "error",
748
+ origin: "https://other.example",
749
+ summary: "b",
750
+ stepid: "s1",
751
+ masked: true,
752
+ maskverdict: "masked",
753
+ hash: { previous: "y", current: "z" },
754
+ at: now + 1,
755
+ },
756
+ ] as never;
757
+ expect(filterlogstream(events, {})).toHaveLength(2);
758
+ expect(filterlogstream(events, { level: "warn" })).toHaveLength(1);
759
+ expect(filterlogstream(events, { origin: "https://example.com" })).toHaveLength(1);
760
+ expect(filterlogstream(events, { stepid: "s1" })).toHaveLength(1);
761
+ });
762
+
763
+ it("returns the bounded live window and keeps the full set when the bound is absent or invalid", () => {
764
+ const events = Array.from({ length: 10 }, (_, index) => ({
765
+ id: `e${index}`,
766
+ level: "info",
767
+ source: "console",
768
+ origin: "o",
769
+ summary: `s${index}`,
770
+ masked: false,
771
+ maskverdict: "",
772
+ hash: { previous: "p", current: "c" },
773
+ at: now + index,
774
+ })) as never;
775
+ expect(livebufferof(events, 5)).toHaveLength(5);
776
+ expect(livebufferof(events, 5)[0]?.id).toBe("e5");
777
+ expect(livebufferof(events, undefined)).toHaveLength(10);
778
+ expect(livebufferof(events, 0)).toHaveLength(10);
779
+ expect(livebufferof(events, -1)).toHaveLength(10);
780
+ expect(livebufferof(events, 1.5)).toHaveLength(10);
781
+ });
782
+
783
+ it("copies the verified excerpt range and refuses the out of range and the unverified range", async () => {
784
+ const first = await logstreameventof({
785
+ level: "info",
786
+ source: "console",
787
+ origin: "https://example.com",
788
+ summary: "first",
789
+ masked: false,
790
+ maskverdict: "",
791
+ previous: logstreamgenesis,
792
+ at: now,
793
+ });
794
+ const second = await logstreameventof({
795
+ level: "info",
796
+ source: "console",
797
+ origin: "https://example.com",
798
+ summary: "second",
799
+ masked: false,
800
+ maskverdict: "",
801
+ previous: first.hash.current,
802
+ at: now + 1,
803
+ });
804
+ const third = await logstreameventof({
805
+ level: "info",
806
+ source: "console",
807
+ origin: "https://example.com",
808
+ summary: "third",
809
+ masked: false,
810
+ maskverdict: "",
811
+ previous: second.hash.current,
812
+ at: now + 2,
813
+ });
814
+ const events = appendlogstreamevent(appendlogstreamevent(appendlogstreamevent([], first), second), third);
815
+ const excerpt = await auditexcerptof(events, { from: 0, to: 2 });
816
+ expect(excerpt.ok).toBe(true);
817
+ expect(excerpt.text).toContain("first");
818
+ const outOfRange = await auditexcerptof(events, { from: 1, to: 10 });
819
+ expect(outOfRange.ok).toBe(false);
820
+ expect(outOfRange.reason).toMatch(/no contiguous slice/i);
821
+ });
822
+
823
+ it("maps the audit kind onto the logstream level", () => {
824
+ expect(loglevelof("error")).toBe("error");
825
+ expect(loglevelof("deny")).toBe("warn");
826
+ expect(loglevelof("revoke")).toBe("warn");
827
+ expect(loglevelof("anything")).toBe("info");
828
+ });
829
+ });