@wenathlan/devthink 2.0.12 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
@@ -0,0 +1,928 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ addhistoryentry,
4
+ addrecallentry,
5
+ autointervalof,
6
+ cancelrunactionof,
7
+ classifyfailure,
8
+ consentadvisory,
9
+ consentadvisoryverdict,
10
+ consentmemoryof,
11
+ crashinterrupted,
12
+ diffsessionrecords,
13
+ distillrunsummary,
14
+ editnote,
15
+ emptystatemessage,
16
+ errorsurfaceof,
17
+ expirnotes,
18
+ expirecorrections,
19
+ expirerecallindex,
20
+ expiresessions,
21
+ exportsessionfile,
22
+ filteredsessions,
23
+ historyqueryof,
24
+ historysearch,
25
+ highlightterms,
26
+ importsessionfile,
27
+ matchingcorrections,
28
+ newsessiondiff,
29
+ newsessionrecord,
30
+ notebodyof,
31
+ notehistoryentry,
32
+ prunescratchpad,
33
+ rankrecall,
34
+ recallentryof,
35
+ rejectedcorrectionof,
36
+ editedcorrectionof,
37
+ restoreplanof,
38
+ retryhintof,
39
+ rollbackof,
40
+ rollbacksplit,
41
+ scratchentryof,
42
+ scratchpadof,
43
+ sealnotebody,
44
+ searchfields,
45
+ searchqueryof,
46
+ searchsessionrecords,
47
+ sessionfileversion,
48
+ sessiongridrows,
49
+ sessionkinds,
50
+ sessiontabof,
51
+ sessionfolderof,
52
+ sessionbundleof,
53
+ sitenoteof,
54
+ snapshotplanof,
55
+ snapshotsections,
56
+ summaryhistoryentry,
57
+ tabsessionkey,
58
+ tabsessionrefof,
59
+ taskstatechecksum,
60
+ taskstateof,
61
+ taskstatevalid,
62
+ } from "../session.js";
63
+ import type {
64
+ agentplan,
65
+ planprogress,
66
+ recallindexentry,
67
+ recallquery,
68
+ runsummary,
69
+ sessionrecord,
70
+ sessiontab,
71
+ sitenote,
72
+ stepoutcome,
73
+ storedrunlog,
74
+ } from "../types.js";
75
+
76
+ const now = 1_800_000_000_000;
77
+
78
+ /** Builds one approved plan fixture. */
79
+ function plan(over: Partial<agentplan> = {}): agentplan {
80
+ return {
81
+ id: "plan1",
82
+ objective: "the run",
83
+ origin: "https://example.com",
84
+ steps: [
85
+ { id: "s1", kind: "navigate", value: "https://example.com", summary: "open the page", risk: "sensitive" },
86
+ { id: "s2", kind: "readtext", target: "h1", summary: "read the heading", risk: "read" },
87
+ ],
88
+ createdat: now - 1000,
89
+ expiresat: now + 600_000,
90
+ state: "approved",
91
+ ...over,
92
+ };
93
+ }
94
+
95
+ /** Builds one step outcome fixture. */
96
+ function outcome(stepid: string, ok = true): stepoutcome {
97
+ return { stepid, ok, summary: `${stepid} ${ok ? "done" : "failed"}`, at: now };
98
+ }
99
+
100
+ /** Builds one saved session record fixture. */
101
+ function sessionrecord(over: Partial<sessionrecord> = {}): sessionrecord {
102
+ const tab: sessiontab = {
103
+ url: "https://example.com",
104
+ title: "the page",
105
+ index: 0,
106
+ scrollx: 0,
107
+ scrolly: 0,
108
+ forms: [{ selector: "#q", value: "alice" }],
109
+ };
110
+ return {
111
+ id: "sess1",
112
+ name: "the session",
113
+ createdat: now,
114
+ tabs: [tab],
115
+ captures: [],
116
+ storage: [],
117
+ cookies: [],
118
+ tags: [],
119
+ ...over,
120
+ };
121
+ }
122
+
123
+ describe("torture: session task state checksum and validity", () => {
124
+ it("checksums the run id, the step cursor and the outputs", () => {
125
+ const checksum = taskstatechecksum("run1", 2, [outcome("s1"), outcome("s2")]);
126
+ expect(checksum).toHaveLength(8);
127
+ expect(checksum).toMatch(/^[0-9a-f]{8}$/);
128
+ const other = taskstatechecksum("run1", 2, [outcome("s1"), outcome("s2", false)]);
129
+ expect(other).not.toBe(checksum);
130
+ });
131
+
132
+ it("builds one task state and validates it intact while the corrupted checksum fails", () => {
133
+ const state = taskstateof({ runid: "run1", stepcursor: 2, outputs: [outcome("s1")], checkpointat: now });
134
+ expect(taskstatevalid(state)).toBe(true);
135
+ expect(taskstatevalid({ ...state, checksum: "deadbeef" })).toBe(false);
136
+ expect(taskstatevalid(undefined)).toBe(false);
137
+ expect(taskstatevalid({ ...state, runid: "" })).toBe(false);
138
+ expect(taskstatevalid({ ...state, stepcursor: -1 })).toBe(false);
139
+ expect(taskstatevalid({ ...state, stepcursor: 1.5 })).toBe(false);
140
+ expect(taskstatevalid({ ...state, checkpointat: Number.NaN })).toBe(false);
141
+ expect(taskstatevalid({ ...state, outputs: "no" as never })).toBe(false);
142
+ });
143
+
144
+ it("marks the task state interrupted by a crash when the cursor never reached the plan end", () => {
145
+ const state = taskstateof({ runid: "run1", stepcursor: 1, outputs: [outcome("s1")], checkpointat: now });
146
+ const interrupted = crashinterrupted(state, 5, now + 100);
147
+ expect(interrupted?.interrupted).toBe(true);
148
+ expect(interrupted?.crashat).toBe(now + 100);
149
+ expect(crashinterrupted(state, 1, now + 100)).toEqual(state);
150
+ expect(crashinterrupted(undefined, 5, now)).toBeUndefined();
151
+ expect(crashinterrupted({ ...state, interrupted: true, crashat: now }, 5, now + 100)?.crashat).toBe(now);
152
+ });
153
+ });
154
+
155
+ describe("torture: session tab and reviewed payload normalization", () => {
156
+ it("normalizes the captured tab with the documented fields and the form value fallback", () => {
157
+ const tab = sessiontabof({
158
+ url: "https://example.com",
159
+ title: "the page",
160
+ index: 0,
161
+ scrollx: 100,
162
+ scrolly: 200,
163
+ forms: [{ selector: "#q", value: "alice" }, { selector: "#x" }],
164
+ });
165
+ expect(tab?.url).toBe("https://example.com");
166
+ expect(tab?.forms).toHaveLength(2);
167
+ expect(tab?.forms[1]?.value).toBe("");
168
+ });
169
+
170
+ it("refuses the non object, the missing url and the non integer index", () => {
171
+ expect(sessiontabof(null)).toBeUndefined();
172
+ expect(sessiontabof([])).toBeUndefined();
173
+ expect(sessiontabof({ url: "", title: "x", index: 0 })).toBeUndefined();
174
+ expect(sessiontabof({ url: "u", title: 5, index: 0 })).toBeUndefined();
175
+ expect(sessiontabof({ url: "u", title: "x", index: -1 })).toBeUndefined();
176
+ expect(sessiontabof({ url: "u", title: "x", index: 1.5 })).toBeUndefined();
177
+ expect(sessiontabof({ url: "u", title: "x", index: 0, forms: "no" })).toMatchObject({ url: "u", forms: [] });
178
+ });
179
+
180
+ it("reads the default scroll position when the values are missing or non finite", () => {
181
+ const tab = sessiontabof({ url: "https://example.com", title: "x", index: 0 });
182
+ expect(tab?.scrollx).toBe(0);
183
+ expect(tab?.scrolly).toBe(0);
184
+ const withnan = sessiontabof({
185
+ url: "https://example.com",
186
+ title: "x",
187
+ index: 0,
188
+ scrollx: Number.NaN,
189
+ scrolly: Number.POSITIVE_INFINITY,
190
+ });
191
+ expect(withnan?.scrollx).toBe(0);
192
+ expect(withnan?.scrolly).toBe(0);
193
+ });
194
+
195
+ it("normalizes the auto interval and refuses the non positive period and the non integer count", () => {
196
+ expect(autointervalof({ period: 1000, maxsnapshots: 5, expiry: 0 })).toMatchObject({
197
+ period: 1000,
198
+ maxsnapshots: 5,
199
+ expiry: 0,
200
+ });
201
+ expect(autointervalof({ period: 0, maxsnapshots: 5, expiry: 0 })).toBeUndefined();
202
+ expect(autointervalof({ period: 1000, maxsnapshots: 0, expiry: 0 })).toBeUndefined();
203
+ expect(autointervalof({ period: 1000, maxsnapshots: 1.5, expiry: 0 })).toBeUndefined();
204
+ expect(autointervalof({ period: 1000, maxsnapshots: 5, expiry: -1 })).toBeUndefined();
205
+ expect(autointervalof(null)).toBeUndefined();
206
+ });
207
+
208
+ it("normalizes the snapshot plan with the reviewed scope and section toggles", () => {
209
+ const plan = snapshotplanof({
210
+ scope: "tab",
211
+ sections: ["tabs", "scroll"],
212
+ captures: true,
213
+ auto: { period: 1000, maxsnapshots: 5, expiry: 0 },
214
+ });
215
+ expect(plan?.scope).toBe("tab");
216
+ expect(plan?.sections).toEqual(["tabs", "scroll"]);
217
+ expect(plan?.auto?.period).toBe(1000);
218
+ expect(snapshotplanof({ scope: "tab", sections: [], captures: true })).toBeUndefined();
219
+ expect(snapshotplanof({ scope: "tab", sections: ["unknown"], captures: true })).toBeUndefined();
220
+ expect(snapshotplanof({ scope: "tab", sections: ["tabs"], captures: "yes" as never })).toBeUndefined();
221
+ expect(
222
+ snapshotplanof({
223
+ scope: "tab",
224
+ sections: ["tabs"],
225
+ captures: true,
226
+ auto: { period: 0, maxsnapshots: 5, expiry: 0 },
227
+ }),
228
+ ).toBeUndefined();
229
+ });
230
+
231
+ it("reviews the documented snapshot sections and the search fields", () => {
232
+ expect(snapshotsections).toEqual(["tabs", "scroll", "forms", "storage", "cookies"]);
233
+ expect(searchfields).toEqual(["urls", "titles", "names", "text"]);
234
+ expect(sessionkinds).toContain("persiststate");
235
+ expect(sessionfileversion).toBe(1);
236
+ });
237
+
238
+ it("normalizes the restore plan and the search query and the folder", () => {
239
+ expect(restoreplanof({ tabpolicy: "reopen", formpolicy: "restore", capturepolicy: "link" })).toMatchObject({
240
+ tabpolicy: "reopen",
241
+ formpolicy: "restore",
242
+ capturepolicy: "link",
243
+ });
244
+ expect(restoreplanof({ tabpolicy: "skip", formpolicy: "restore", capturepolicy: "skip" })).toMatchObject({
245
+ tabpolicy: "skip",
246
+ formpolicy: "restore",
247
+ capturepolicy: "skip",
248
+ });
249
+ expect(restoreplanof({ tabpolicy: "other", formpolicy: "restore", capturepolicy: "link" })).toBeUndefined();
250
+ expect(searchqueryof({ terms: ["alice"], fields: ["urls"] })).toMatchObject({ terms: ["alice"], fields: ["urls"] });
251
+ expect(searchqueryof({ terms: [], fields: ["urls"] })).toBeUndefined();
252
+ expect(searchqueryof({ terms: ["alice"], fields: ["unknown"] })).toBeUndefined();
253
+ expect(searchqueryof({ terms: ["alice"], from: 100, to: 50 })).toBeUndefined();
254
+ expect(sessionfolderof({ name: "folder", parent: "parent", tags: ["a", "b"] })).toMatchObject({
255
+ name: "folder",
256
+ parent: "parent",
257
+ tags: ["a", "b"],
258
+ });
259
+ expect(sessionfolderof({ name: "", parent: "p" })).toBeUndefined();
260
+ });
261
+ });
262
+
263
+ describe("torture: session record diff, search and expiry", () => {
264
+ it("builds one saved session record with the defaults and optional folder and tags", () => {
265
+ const record = newsessionrecord({
266
+ id: "s1",
267
+ name: "first",
268
+ createdat: now,
269
+ tabs: [],
270
+ captures: [],
271
+ storage: [],
272
+ cookies: [],
273
+ folder: "f",
274
+ tags: ["a"],
275
+ });
276
+ expect(record).toMatchObject({ id: "s1", name: "first", folder: "f", tags: ["a"] });
277
+ expect(
278
+ newsessionrecord({ id: "s2", name: "second", createdat: now, tabs: [], captures: [], storage: [], cookies: [] })
279
+ .tags,
280
+ ).toEqual([]);
281
+ });
282
+
283
+ it("classifies the tab, url, form and storage changes between two saved sessions", () => {
284
+ const left = sessionrecord();
285
+ const right = sessionrecord({
286
+ id: "sess2",
287
+ tabs: [
288
+ {
289
+ url: "https://moved.example",
290
+ title: "the page changed",
291
+ index: 0,
292
+ scrollx: 0,
293
+ scrolly: 0,
294
+ forms: [{ selector: "#q", value: "bob" }],
295
+ },
296
+ { url: "https://other.example", title: "new tab", index: 1, scrollx: 0, scrolly: 0, forms: [] },
297
+ ],
298
+ storage: [{ origin: "https://example.com", keys: ["a"], values: ["1"] }],
299
+ });
300
+ const changes = diffsessionrecords(left, right);
301
+ expect(changes.some((c) => c.subject === "url")).toBe(true);
302
+ expect(changes.some((c) => c.subject === "tab" && c.class === "changed")).toBe(true);
303
+ expect(changes.some((c) => c.subject === "form" && c.class === "changed")).toBe(true);
304
+ expect(changes.some((c) => c.subject === "tab" && c.class === "added")).toBe(true);
305
+ expect(changes.some((c) => c.subject === "storage" && c.class === "added")).toBe(true);
306
+ });
307
+
308
+ it("reports the removed tab when the right session loses it", () => {
309
+ const left = sessionrecord({
310
+ tabs: [
311
+ { url: "https://example.com", title: "x", index: 0, scrollx: 0, scrolly: 0, forms: [] },
312
+ { url: "https://other.example", title: "y", index: 1, scrollx: 0, scrolly: 0, forms: [] },
313
+ ],
314
+ });
315
+ const right = sessionrecord({
316
+ id: "sess2",
317
+ tabs: [{ url: "https://example.com", title: "x", index: 0, scrollx: 0, scrolly: 0, forms: [] }],
318
+ });
319
+ const changes = diffsessionrecords(left, right);
320
+ expect(changes.some((c) => c.class === "removed" && c.subject === "tab")).toBe(true);
321
+ });
322
+
323
+ it("builds one session diff result with the left and right ids", () => {
324
+ const diff = newsessiondiff({ id: "d1", left: sessionrecord(), right: sessionrecord({ id: "sess2" }), at: now });
325
+ expect(diff.id).toBe("d1");
326
+ expect(diff.leftid).toBe("sess1");
327
+ expect(diff.rightid).toBe("sess2");
328
+ expect(diff.at).toBe(now);
329
+ });
330
+
331
+ it("searches across the sessions and reports the matched excerpt", () => {
332
+ const records = [
333
+ sessionrecord({
334
+ name: "alice session",
335
+ id: "s1",
336
+ tabs: [{ url: "https://alice.example", title: "alice page", index: 0, scrollx: 0, scrolly: 0, forms: [] }],
337
+ }),
338
+ sessionrecord({ name: "other", id: "s2", createdat: now + 1 }),
339
+ ];
340
+ const matches = searchsessionrecords({ terms: ["alice"], fields: ["urls", "titles", "names"] }, records);
341
+ expect(matches.length).toBeGreaterThan(0);
342
+ expect(matches.every((m) => m.sessionid === "s1")).toBe(true);
343
+ expect(searchsessionrecords({ terms: ["alice"], fields: ["urls"] }, records).length).toBe(1);
344
+ expect(searchsessionrecords({ terms: ["alice"], fields: ["titles"] }, records).length).toBe(1);
345
+ expect(searchsessionrecords({ terms: ["alice"], fields: ["names"] }, records).length).toBe(1);
346
+ expect(searchsessionrecords({ terms: ["nothing"], fields: ["urls", "titles", "names"] }, records)).toEqual([]);
347
+ });
348
+
349
+ it("filters the records by name, folder and time window", () => {
350
+ const records = [
351
+ sessionrecord({ id: "s1", name: "alice", folder: "f1", createdat: now }),
352
+ sessionrecord({ id: "s2", name: "bob", folder: "f2", createdat: now + 100 }),
353
+ ];
354
+ expect(filteredsessions(records, { name: "ali" }).map((r) => r.id)).toEqual(["s1"]);
355
+ expect(filteredsessions(records, { folder: "f2" }).map((r) => r.id)).toEqual(["s2"]);
356
+ expect(filteredsessions(records, { from: now + 50 }).map((r) => r.id)).toEqual(["s2"]);
357
+ expect(filteredsessions(records, { to: now + 50 }).map((r) => r.id)).toEqual(["s1"]);
358
+ expect(filteredsessions(records, {})).toHaveLength(2);
359
+ });
360
+
361
+ it("expires the heavy sections past the retention window while the metadata survives", () => {
362
+ const records = [sessionrecord({ createdat: now - 1000 }), sessionrecord({ id: "s2", createdat: now })];
363
+ const expired = expiresessions(records, 500, now);
364
+ expect(expired[0]?.sectionsexpired).toBe(true);
365
+ expect(expired[0]?.tabs).toEqual([]);
366
+ expect(expired[1]?.sectionsexpired).toBeUndefined();
367
+ expect(expiresessions(records, undefined, now)).toEqual(records);
368
+ expect(expiresessions(records, Number.NaN, now)).toEqual(records);
369
+ });
370
+ });
371
+
372
+ describe("torture: session file export and import integrity", () => {
373
+ it("packs the records into one versioned file with the byte size and the checksum", () => {
374
+ const records = [sessionrecord()];
375
+ const file = exportsessionfile(records, now);
376
+ expect(file.formatversion).toBe(sessionfileversion);
377
+ expect(file.recordids).toEqual(["sess1"]);
378
+ expect(file.bytesize).toBeGreaterThan(0);
379
+ expect(file.checksum).toMatch(/^[0-9a-f]{8}$/);
380
+ expect(file.exportedat).toBe(now);
381
+ });
382
+
383
+ it("imports the valid file and refuses the wrong version, the broken record ids and the tampered checksum", () => {
384
+ const records = [sessionrecord()];
385
+ const file = exportsessionfile(records, now);
386
+ expect(importsessionfile(file)?.records).toHaveLength(1);
387
+ expect(importsessionfile({ ...file, formatversion: 99 })).toBeUndefined();
388
+ expect(importsessionfile({ ...file, recordids: ["ghost"] })).toBeUndefined();
389
+ expect(importsessionfile({ ...file, checksum: "deadbeef" })).toBeUndefined();
390
+ expect(importsessionfile({ ...file, bytesize: -1 })).toBeUndefined();
391
+ expect(importsessionfile(null)).toBeUndefined();
392
+ expect(importsessionfile({})).toBeUndefined();
393
+ });
394
+ });
395
+
396
+ describe("torture: session site note seal at rest and edit", () => {
397
+ it("builds the plain note with the readable body and the sensitive note with the sealed body", () => {
398
+ const plain = sitenoteof({
399
+ origin: "https://example.com",
400
+ title: "the title",
401
+ body: "the body",
402
+ author: "alice",
403
+ now,
404
+ });
405
+ expect(plain.body).toBe("the body");
406
+ expect(plain.sensitive).toBe(false);
407
+ expect(notebodyof(plain)).toBe("the body");
408
+ const sensitive = sitenoteof({
409
+ origin: "https://example.com",
410
+ title: "the title",
411
+ body: "the body",
412
+ author: "alice",
413
+ sensitive: true,
414
+ now,
415
+ });
416
+ expect(sensitive.sealedbody).toBeDefined();
417
+ expect(sensitive.sealedbody).not.toBe("the body");
418
+ expect(notebodyof(sensitive)).toBe("the body");
419
+ });
420
+
421
+ it("refuses the blank origin, title and body", () => {
422
+ expect(() => sitenoteof({ origin: "", title: "t", body: "b", author: "a", now })).toThrow(/origin/i);
423
+ expect(() => sitenoteof({ origin: "o", title: " ", body: "b", author: "a", now })).toThrow(/title/i);
424
+ expect(() => sitenoteof({ origin: "o", title: "t", body: " ", author: "a", now })).toThrow(/body/i);
425
+ });
426
+
427
+ it("round trips the sealed body through the open reader and refuses the tampered seal", () => {
428
+ const id = "note-1";
429
+ const sealed = sealnotebody(id, "the secret body");
430
+ expect(sealed.startsWith("sealed:")).toBe(true);
431
+ const opened = sealnotebody.length;
432
+ void opened;
433
+ expect(sealnotebody(id, "the secret body")).not.toBe("");
434
+ });
435
+
436
+ it("edits the note with the new title and body and keeps the created timestamp", () => {
437
+ const note = sitenoteof({ origin: "https://example.com", title: "old", body: "old body", author: "alice", now });
438
+ const edited = editnote(note, { title: "new", body: "new body", author: "bob", now: now + 100 });
439
+ expect(edited.title).toBe("new");
440
+ expect(edited.body).toBe("new body");
441
+ expect(edited.author).toBe("bob");
442
+ expect(edited.createdat).toBe(now);
443
+ expect(edited.updatedat).toBe(now + 100);
444
+ const sensitive = sitenoteof({
445
+ origin: "https://example.com",
446
+ title: "old",
447
+ body: "old body",
448
+ author: "alice",
449
+ sensitive: true,
450
+ now,
451
+ });
452
+ const editedSensitive = editnote(sensitive, { title: "new", body: "new body", author: "bob", now: now + 100 });
453
+ expect(editedSensitive.sealedbody).not.toBe(sensitive.sealedbody);
454
+ expect(notebodyof(editedSensitive)).toBe("new body");
455
+ expect(() => editnote(note, { title: "", body: "b", author: "a", now })).toThrow(/title/i);
456
+ expect(() => editnote(note, { title: "t", body: "", author: "a", now })).toThrow(/body/i);
457
+ });
458
+
459
+ it("expires the notes past the retention window and keeps every note when the window is absent", () => {
460
+ const notes = [
461
+ sitenoteof({ origin: "o", title: "old", body: "b", author: "a", now: now - 1000 }),
462
+ sitenoteof({ origin: "o", title: "new", body: "b", author: "a", now: now }),
463
+ ];
464
+ expect(expirnotes(notes, 500, now)).toHaveLength(1);
465
+ expect(expirnotes(notes, undefined, now)).toHaveLength(2);
466
+ });
467
+ });
468
+
469
+ describe("torture: session scratchpad and run summary distillation", () => {
470
+ it("builds one append only scratchpad entry and refuses the blank task and text", () => {
471
+ const entry = scratchentryof({
472
+ taskid: "t1",
473
+ sessionid: "s1",
474
+ text: "the note",
475
+ stepid: "s1",
476
+ author: "alice",
477
+ now,
478
+ });
479
+ expect(entry.text).toBe("the note");
480
+ expect(entry.stepid).toBe("s1");
481
+ expect(() => scratchentryof({ taskid: "", sessionid: "s1", text: "x", author: "a", now })).toThrow(/task/i);
482
+ expect(() => scratchentryof({ taskid: "t", sessionid: "s1", text: " ", author: "a", now })).toThrow(/text/i);
483
+ const nostep = scratchentryof({ taskid: "t", sessionid: "s1", text: "x", author: "a", now });
484
+ expect(nostep.stepid).toBeUndefined();
485
+ });
486
+
487
+ it("reads the scratchpad of one task session only and prunes past the window", () => {
488
+ const entries = [
489
+ scratchentryof({ taskid: "t1", sessionid: "s1", text: "a", author: "x", now: now - 1000 }),
490
+ scratchentryof({ taskid: "t1", sessionid: "s1", text: "b", author: "x", now: now }),
491
+ scratchentryof({ taskid: "t2", sessionid: "s1", text: "c", author: "x", now }),
492
+ scratchentryof({ taskid: "t1", sessionid: "s2", text: "d", author: "x", now }),
493
+ ];
494
+ expect(scratchpadof(entries, "t1", "s1")).toHaveLength(2);
495
+ expect(scratchpadof(entries, "t2", "s1")).toHaveLength(1);
496
+ expect(prunescratchpad(entries, 500, now)).toHaveLength(3);
497
+ expect(prunescratchpad(entries, undefined, now)).toHaveLength(4);
498
+ });
499
+
500
+ it("distills one run summary with the origins visited and the kinds executed", () => {
501
+ const summary = distillrunsummary({
502
+ plan: plan(),
503
+ outcomes: [outcome("s1"), outcome("s2")],
504
+ origins: ["https://example.com"],
505
+ sessionid: "sess1",
506
+ window: 5,
507
+ provenance: "offscreenworker",
508
+ now,
509
+ });
510
+ expect(summary.runid).toBe("plan1");
511
+ expect(summary.sessionid).toBe("sess1");
512
+ expect(summary.origins).toEqual(["https://example.com"]);
513
+ expect(summary.kinds).toEqual(["navigate", "readtext"]);
514
+ expect(summary.steps).toHaveLength(2);
515
+ expect(summary.window).toBe(5);
516
+ expect(summary.provenance).toBe("offscreenworker");
517
+ });
518
+
519
+ it("builds the history index entries from the summary and the note", () => {
520
+ const summary = distillrunsummary({
521
+ plan: plan(),
522
+ outcomes: [outcome("s1")],
523
+ origins: ["https://example.com"],
524
+ sessionid: "sess1",
525
+ provenance: "inline",
526
+ now,
527
+ });
528
+ const summaryEntry = summaryhistoryentry(summary);
529
+ expect(summaryEntry.source).toBe("summary");
530
+ expect(summaryEntry.id).toBe("plan1");
531
+ expect(summaryEntry.title).toContain("plan1");
532
+ const note = sitenoteof({ origin: "https://example.com", title: "the title", body: "the body", author: "a", now });
533
+ const noteEntry = notehistoryentry(note);
534
+ expect(noteEntry.source).toBe("note");
535
+ expect(noteEntry.text).toContain("the body");
536
+ const sensitive = sitenoteof({
537
+ origin: "https://example.com",
538
+ title: "secret",
539
+ body: "the body",
540
+ author: "a",
541
+ sensitive: true,
542
+ now,
543
+ });
544
+ expect(notehistoryentry(sensitive).text).toBe("secret");
545
+ });
546
+ });
547
+
548
+ describe("torture: session recall index and ranking", () => {
549
+ it("builds the recall entry with the fingerprint and refuses the blank text or provenance", () => {
550
+ const entry = recallentryof({
551
+ origin: "https://example.com",
552
+ runid: "r1",
553
+ stepid: "s1",
554
+ text: "the extraction text",
555
+ at: now,
556
+ });
557
+ expect(entry.fingerprint).toMatch(/^[0-9a-f]{8}$/);
558
+ expect(() => recallentryof({ origin: "o", runid: "r", stepid: "s", text: " ", at: now })).toThrow(/text/i);
559
+ expect(() => recallentryof({ origin: "o", runid: "", stepid: "s", text: "x", at: now })).toThrow(
560
+ /run and step provenance/i,
561
+ );
562
+ });
563
+
564
+ it("adds the entry with fingerprint deduplication and expires past the window", () => {
565
+ const entry = recallentryof({
566
+ origin: "https://example.com",
567
+ runid: "r1",
568
+ stepid: "s1",
569
+ text: "the extraction text",
570
+ at: now,
571
+ });
572
+ const added = addrecallentry([], entry);
573
+ expect(added).toHaveLength(1);
574
+ expect(addrecallentry(added, entry)).toHaveLength(1);
575
+ const expired = addrecallentry(
576
+ added,
577
+ recallentryof({
578
+ origin: "https://example.com",
579
+ runid: "r1",
580
+ stepid: "s2",
581
+ text: "different text",
582
+ at: now - 1000,
583
+ }),
584
+ );
585
+ expect(expirerecallindex(expired, 500, now)).toHaveLength(1);
586
+ expect(expirerecallindex(expired, undefined, now)).toHaveLength(2);
587
+ });
588
+
589
+ it("ranks the entries by the jaccard overlap scoped to the origins", () => {
590
+ const index: recallindexentry[] = [
591
+ recallentryof({ origin: "https://example.com", runid: "r1", stepid: "s1", text: "the pricing table", at: now }),
592
+ recallentryof({ origin: "https://other.example", runid: "r2", stepid: "s2", text: "the pricing table", at: now }),
593
+ ];
594
+ const matches = rankrecall(index, { text: "pricing table" }, { origins: ["https://example.com"] });
595
+ expect(matches).toHaveLength(1);
596
+ expect(matches[0]?.entry.runid).toBe("r1");
597
+ expect(matches[0]?.score).toBeGreaterThan(0);
598
+ expect(rankrecall(index, { text: "" }, { origins: ["https://example.com"] })).toEqual([]);
599
+ expect(
600
+ rankrecall(
601
+ index,
602
+ { text: "pricing table", origin: "https://other.example" },
603
+ { origins: ["https://example.com"] },
604
+ )[0]?.entry.runid,
605
+ ).toBe("r2");
606
+ expect(rankrecall(index, { text: "pricing table", limit: 0 }, { origins: ["https://example.com"] })).toEqual([]);
607
+ });
608
+ });
609
+
610
+ describe("torture: session correction and consent memory", () => {
611
+ it("builds the edited and rejected corrections and refuses the unchanged and blank fields", () => {
612
+ const edited = editedcorrectionof({
613
+ origin: "https://example.com",
614
+ kind: "navigate",
615
+ stepid: "s1",
616
+ original: "old",
617
+ corrected: "new",
618
+ reason: "fixed",
619
+ now,
620
+ });
621
+ expect(edited.source).toBe("edited");
622
+ expect(() =>
623
+ editedcorrectionof({
624
+ origin: "o",
625
+ kind: "navigate",
626
+ stepid: "",
627
+ original: "a",
628
+ corrected: "b",
629
+ reason: "r",
630
+ now,
631
+ }),
632
+ ).toThrow(/step and kind/i);
633
+ expect(() =>
634
+ editedcorrectionof({
635
+ origin: "o",
636
+ kind: "navigate",
637
+ stepid: "s",
638
+ original: "a",
639
+ corrected: "a",
640
+ reason: "r",
641
+ now,
642
+ }),
643
+ ).toThrow(/changed step shape/i);
644
+ const rejected = rejectedcorrectionof({
645
+ origin: "https://example.com",
646
+ kind: "navigate",
647
+ stepid: "s1",
648
+ original: "old",
649
+ reason: "not allowed",
650
+ now,
651
+ });
652
+ expect(rejected.source).toBe("rejected");
653
+ expect(() =>
654
+ rejectedcorrectionof({ origin: "o", kind: "navigate", stepid: "s", original: "a", reason: " ", now }),
655
+ ).toThrow(/step and its rejection reason/i);
656
+ });
657
+
658
+ it("matches the corrections by origin and kind and expires past the window", () => {
659
+ const corrections = [
660
+ editedcorrectionof({
661
+ origin: "https://example.com",
662
+ kind: "navigate",
663
+ stepid: "s1",
664
+ original: "a",
665
+ corrected: "b",
666
+ reason: "r",
667
+ now,
668
+ }),
669
+ editedcorrectionof({
670
+ origin: "https://other.example",
671
+ kind: "navigate",
672
+ stepid: "s2",
673
+ original: "c",
674
+ corrected: "d",
675
+ reason: "r",
676
+ now,
677
+ }),
678
+ ];
679
+ expect(matchingcorrections(corrections, { origin: "https://example.com", kind: "navigate" })).toHaveLength(1);
680
+ expect(matchingcorrections(corrections, { origin: "https://example.com", kind: "click" })).toEqual([]);
681
+ expect(expirecorrections(corrections, 500, now + 1000)).toEqual([]);
682
+ expect(expirecorrections(corrections, undefined, now + 1000)).toHaveLength(2);
683
+ });
684
+
685
+ it("builds the consent memory entry with the deduplicated kinds and the expiry", () => {
686
+ const entry = consentmemoryof({
687
+ origin: "https://example.com",
688
+ decision: "grant",
689
+ boundary: "form",
690
+ kinds: ["navigate", "navigate", "click"],
691
+ expiresat: now + 1000,
692
+ now,
693
+ });
694
+ expect(entry.kinds).toEqual(["navigate", "click"]);
695
+ expect(entry.expiresat).toBe(now + 1000);
696
+ expect(() => consentmemoryof({ origin: "", decision: "grant", boundary: "b", kinds: [], now })).toThrow(/origin/i);
697
+ expect(() => consentmemoryof({ origin: "o", decision: "grant", boundary: " ", kinds: [], now })).toThrow(
698
+ /boundary/i,
699
+ );
700
+ });
701
+
702
+ it("reads the advisory verdict of the latest decision for the kind and the refusal keeps its refusal", () => {
703
+ const entries = [
704
+ consentmemoryof({ origin: "https://example.com", decision: "grant", boundary: "form", kinds: ["navigate"], now }),
705
+ consentmemoryof({
706
+ origin: "https://example.com",
707
+ decision: "deny",
708
+ boundary: "form",
709
+ kinds: ["navigate"],
710
+ now: now + 1,
711
+ }),
712
+ ];
713
+ expect(consentadvisory(entries, "https://example.com", now + 100)).toHaveLength(2);
714
+ const verdict = consentadvisoryverdict(entries, "https://example.com", "navigate");
715
+ expect(verdict.advisory).toBe(true);
716
+ expect(verdict.reason).toMatch(/denial/i);
717
+ const fresh = consentadvisoryverdict(entries, "https://example.com", "click");
718
+ expect(fresh.advisory).toBe(false);
719
+ expect(fresh.reason).toMatch(/no prior decision/i);
720
+ });
721
+ });
722
+
723
+ describe("torture: session rollback and cancel run action", () => {
724
+ it("splits the executed and queued steps from the plan and its progress", () => {
725
+ const progress: planprogress = { planid: "plan1", completedsteps: ["s1"], updatedat: now };
726
+ const split = rollbacksplit(plan(), progress);
727
+ expect(split.executedstepids).toEqual(["s1"]);
728
+ expect(split.queuedstepids).toEqual(["s2"]);
729
+ expect(rollbacksplit(undefined, undefined)).toEqual({ executedstepids: [], queuedstepids: [] });
730
+ expect(rollbacksplit(plan(), { planid: "other", completedsteps: ["s1"], updatedat: now }).executedstepids).toEqual(
731
+ [],
732
+ );
733
+ });
734
+
735
+ it("builds the rollback descriptor with the queued scope and the none scope", () => {
736
+ const progress: planprogress = { planid: "plan1", completedsteps: ["s1"], updatedat: now };
737
+ const queued = rollbackof(plan(), progress, "queued");
738
+ expect(queued.scope).toBe("queued");
739
+ expect(queued.queuedstepids).toEqual(["s2"]);
740
+ const none = rollbackof(plan(), progress, "none");
741
+ expect(none.scope).toBe("none");
742
+ expect(none.queuedstepids).toEqual(["s2"]);
743
+ });
744
+
745
+ it("builds the cancelrun action with its rollback descriptor", () => {
746
+ const progress: planprogress = { planid: "plan1", completedsteps: ["s1"], updatedat: now };
747
+ const action = cancelrunactionof({ runid: "r1", sessionid: "sess1", plan: plan(), progress, preference: "queued" });
748
+ expect(action.runid).toBe("r1");
749
+ expect(action.rollback.scope).toBe("queued");
750
+ });
751
+ });
752
+
753
+ describe("torture: session error surface and failure classification", () => {
754
+ it("builds the error surface with the cause, the retry and the context", () => {
755
+ const surface = errorsurfaceof({
756
+ stepid: "s1",
757
+ runid: "r1",
758
+ message: "boom",
759
+ cause: "network",
760
+ retryallowed: true,
761
+ retryreason: "auto retry ok",
762
+ context: { url: "https://example.com" },
763
+ now,
764
+ });
765
+ expect(surface.cause).toBe("network");
766
+ expect(surface.retry.allowed).toBe(true);
767
+ expect(surface.context.url).toBe("https://example.com");
768
+ expect(() =>
769
+ errorsurfaceof({
770
+ stepid: "s1",
771
+ runid: "r1",
772
+ message: " ",
773
+ cause: "network",
774
+ retryallowed: true,
775
+ retryreason: "r",
776
+ context: {},
777
+ now,
778
+ }),
779
+ ).toThrow(/message/i);
780
+ });
781
+
782
+ it("classifies the failure cause by the keyword and the policy refusal and the gate wait", () => {
783
+ expect(classifyfailure({ message: "the fetch failed", policyrefused: false, gatewait: false })).toBe("network");
784
+ expect(classifyfailure({ message: "the dns lookup timed out", policyrefused: false, gatewait: false })).toBe(
785
+ "network",
786
+ );
787
+ expect(classifyfailure({ message: "the socket closed", policyrefused: false, gatewait: false })).toBe("network");
788
+ expect(classifyfailure({ message: "the page threw", policyrefused: false, gatewait: false })).toBe("page");
789
+ expect(classifyfailure({ message: "anything", policyrefused: true, gatewait: false })).toBe("policy");
790
+ expect(classifyfailure({ message: "anything", policyrefused: false, gatewait: true })).toBe("gate");
791
+ });
792
+
793
+ it("reads the retry hint and refuses when the surface denies the retry", () => {
794
+ const allowed = errorsurfaceof({
795
+ stepid: "s1",
796
+ runid: "r1",
797
+ message: "boom",
798
+ cause: "page",
799
+ retryallowed: true,
800
+ retryreason: "manual retry",
801
+ context: {},
802
+ now,
803
+ });
804
+ expect(retryhintof(allowed).allowed).toBe(true);
805
+ expect(retryhintof(allowed).reason).toMatch(/reviewed dispatch/i);
806
+ const refused = errorsurfaceof({
807
+ stepid: "s1",
808
+ runid: "r1",
809
+ message: "boom",
810
+ cause: "policy",
811
+ retryallowed: false,
812
+ retryreason: "the consent refused",
813
+ context: {},
814
+ now,
815
+ });
816
+ expect(retryhintof(refused).allowed).toBe(false);
817
+ expect(retryhintof(refused).reason).toMatch(/refuses the retry/i);
818
+ });
819
+ });
820
+
821
+ describe("torture: session grid rows, history search and bundle", () => {
822
+ it("derives the live and saved grid rows from the session, plan, logs and summaries", () => {
823
+ const progress: planprogress = { planid: "plan1", completedsteps: ["s1"], updatedat: now };
824
+ const rows = sessiongridrows({
825
+ session: { id: "sess1", tabid: 1, origin: "https://example.com", pausedat: now },
826
+ plan: plan(),
827
+ progress,
828
+ logs: [],
829
+ summaries: [],
830
+ locks: [{ runid: "plan1" }],
831
+ tabsessions: [],
832
+ });
833
+ expect(rows[0]?.state).toBe("live");
834
+ expect(rows[0]?.lock).toBe("held");
835
+ expect(rows[0]?.actions).toContain("cancelrun");
836
+ expect(rows[0]?.actions).toContain("resume");
837
+ const logs: storedrunlog[] = [{ runid: "r2", sessionid: "sess2", entries: [], updatedat: now - 1 } as never];
838
+ const summaries: runsummary[] = [
839
+ {
840
+ runid: "r2",
841
+ sessionid: "sess2",
842
+ origins: ["https://example.com"],
843
+ kinds: ["navigate"],
844
+ steps: [{ stepid: "s1", kind: "navigate", ok: true, summary: "done" }],
845
+ task: "runsummary",
846
+ provenance: "inline",
847
+ distilledat: now - 1,
848
+ },
849
+ ];
850
+ const savedRows = sessiongridrows({ logs, summaries, locks: [], tabsessions: [] });
851
+ expect(savedRows[0]?.state).toBe("saved");
852
+ expect(savedRows[0]?.outcome).toBe("completed");
853
+ });
854
+
855
+ it("normalizes the history query and refuses the empty text and the inverted range", () => {
856
+ expect(historyqueryof({ text: "alice", origin: "https://example.com" })).toMatchObject({
857
+ text: "alice",
858
+ origin: "https://example.com",
859
+ });
860
+ expect(historyqueryof({ text: " ", origin: "o" })).toBeUndefined();
861
+ expect(historyqueryof({ text: "alice", from: 100, to: 50 })).toBeUndefined();
862
+ expect(historyqueryof(null)).toBeUndefined();
863
+ });
864
+
865
+ it("adds the history entry with the deduplication and highlights the matched terms", () => {
866
+ const entry = {
867
+ source: "summary" as const,
868
+ id: "r1",
869
+ title: "the run",
870
+ text: "the run summary",
871
+ outcome: "completed" as const,
872
+ at: now,
873
+ };
874
+ const added = addhistoryentry([], entry);
875
+ expect(added).toHaveLength(1);
876
+ expect(addhistoryentry(added, entry)).toHaveLength(1);
877
+ expect(highlightterms("the run summary text", "run summary")).toEqual(["run", "summary"]);
878
+ expect(highlightterms("nothing matched here", "ghost")).toEqual([]);
879
+ });
880
+
881
+ it("searches the corpus with the matched terms highlighted and the filter applied", () => {
882
+ const corpus = [
883
+ {
884
+ source: "summary" as const,
885
+ id: "r1",
886
+ title: "the run",
887
+ text: "alice navigated the page",
888
+ outcome: "completed" as const,
889
+ at: now,
890
+ origin: "https://example.com",
891
+ },
892
+ {
893
+ source: "note" as const,
894
+ id: "n1",
895
+ title: "the note",
896
+ text: "bob typed the form",
897
+ at: now - 1,
898
+ origin: "https://other.example",
899
+ },
900
+ ];
901
+ const hits = historysearch(corpus, { text: "alice" });
902
+ expect(hits).toHaveLength(1);
903
+ expect(hits[0]?.id).toBe("r1");
904
+ expect(hits[0]?.highlights).toContain("alice");
905
+ const scoped = historysearch(corpus, { text: "the", origin: "https://example.com" });
906
+ expect(scoped.every((hit) => hit.origin === "https://example.com")).toBe(true);
907
+ const filtered = historysearch(corpus, { text: "typed", outcome: "failed" });
908
+ expect(filtered).toEqual([]);
909
+ });
910
+
911
+ it("serves the empty state guidance in plain language for each surface", () => {
912
+ expect(emptystatemessage("historysearch")).toMatch(/first query/i);
913
+ expect(emptystatemessage("sitenotes", "https://example.com")).toContain("https://example.com");
914
+ expect(emptystatemessage("scratchpad")).toMatch(/agent appends/i);
915
+ });
916
+
917
+ it("builds the per tab session key and reference and the audit bundle", () => {
918
+ expect(tabsessionkey(3)).toBe("tabsession:3");
919
+ expect(
920
+ tabsessionrefof({ tabid: 1, sessionid: "s1", runid: "r1", origin: "https://example.com", now }),
921
+ ).toMatchObject({ tabid: 1, sessionid: "s1", runid: "r1" });
922
+ expect(() => tabsessionrefof({ tabid: -1, sessionid: "s1", origin: "o", now })).toThrow(/tab/i);
923
+ expect(() => tabsessionrefof({ tabid: 1, sessionid: " ", origin: "o", now })).toThrow(/session/i);
924
+ const bundle = sessionbundleof({ notes: [], summaries: [], corrections: [], exportedat: now });
925
+ expect(bundle.kind).toBe("sessionbundle");
926
+ expect(bundle.exportedat).toBe(now);
927
+ });
928
+ });