@wenathlan/devthink 2.0.15 → 2.0.20

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 +54 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
@@ -1,4155 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { sessionmemory } from "../memory.js";
3
- import type {
4
- bodyrecord,
5
- callrecord,
6
- costbudget,
7
- lessonrecord,
8
- plandraft,
9
- prompttemplate,
10
- providerconfig,
11
- reflectnote,
12
- replanrecord,
13
- usagerecord,
14
- } from "../types.js";
15
-
16
- class fakeadapter {
17
- private readonly data = new Map<string, unknown>();
18
- async get<T>(key: string): Promise<T | undefined> {
19
- return this.data.get(key) as T | undefined;
20
- }
21
- async set<T>(key: string, value: T): Promise<void> {
22
- this.data.set(key, value);
23
- }
24
- }
25
-
26
- describe("sessionmemory", () => {
27
- it("keeps an ordered audit trail with unlimited default retention", async () => {
28
- const store = new sessionmemory(new fakeadapter());
29
- await store.addaudi({ id: "one", kind: "observe", at: 1, summary: "first" });
30
- await store.addaudi({ id: "two", kind: "stop", at: 2, summary: "second" });
31
- expect((await store.getaudit()).map((event) => event.id)).toEqual(["two", "one"]);
32
- });
33
-
34
- it("applies the user configured audit retention window", async () => {
35
- const store = new sessionmemory(new fakeadapter());
36
- await store.setsettings({ auditretention: 2 });
37
- for (const id of ["one", "two", "three"]) await store.addaudi({ id, kind: "observe", at: 1, summary: id });
38
- expect((await store.getaudit()).map((event) => event.id)).toEqual(["three", "two"]);
39
- });
40
-
41
- it("keeps one structured diagnostics record", async () => {
42
- const store = new sessionmemory(new fakeadapter());
43
- await store.setdiagnostic({
44
- id: "health",
45
- sessionid: "session",
46
- origin: "https://example.com",
47
- capturedat: 1,
48
- tabid: 2,
49
- title: "Example",
50
- textlength: 34,
51
- interactivecount: 5,
52
- formcount: 1,
53
- bridgeavailable: true,
54
- });
55
- expect((await store.getdiagnostic())?.bridgeavailable).toBe(true);
56
- });
57
-
58
- it("stores step outcomes with unlimited default retention", async () => {
59
- const store = new sessionmemory(new fakeadapter());
60
- await store.addoutcome({ stepid: "one", ok: true, summary: "Done.", details: { count: 1 }, at: 1 });
61
- await store.addoutcome({ stepid: "two", ok: false, summary: "Failed.", at: 2 });
62
- const outcomes = await store.getoutcomes();
63
- expect(outcomes.map((outcome) => outcome.stepid)).toEqual(["two", "one"]);
64
- expect(outcomes[1]?.details?.count).toBe(1);
65
- });
66
-
67
- it("stores the negotiated capability report", async () => {
68
- const store = new sessionmemory(new fakeadapter());
69
- await store.setcapabilities({
70
- tabs: true,
71
- downloads: false,
72
- clipboardread: false,
73
- clipboardwrite: false,
74
- reportedat: 7,
75
- });
76
- expect((await store.getcapabilities())?.tabs).toBe(true);
77
- });
78
-
79
- it("stores every clickable map under its observation version", async () => {
80
- const store = new sessionmemory(new fakeadapter());
81
- expect(await store.getobservationversion()).toBeUndefined();
82
- const first = await store.nextobservationversion();
83
- const second = await store.nextobservationversion();
84
- expect(first).toBe(1);
85
- expect(second).toBe(2);
86
- expect(await store.getobservationversion()).toBe(2);
87
- await store.setmap({
88
- version: first,
89
- builtat: 10,
90
- entries: [{ number: 1, selector: "#go", role: "button", label: "Go", mode: "selector" }],
91
- });
92
- await store.setmap({
93
- version: second,
94
- builtat: 20,
95
- entries: [
96
- { number: 1, selector: "#go", role: "button", label: "Go", mode: "selector" },
97
- { number: 2, selector: "a:nth-of-type(1)", role: "link", label: "Home", mode: "selector" },
98
- ],
99
- });
100
- expect((await store.getmap(first))?.entries).toHaveLength(1);
101
- expect((await store.getmap(second))?.entries[1]).toMatchObject({
102
- number: 2,
103
- selector: "a:nth-of-type(1)",
104
- role: "link",
105
- label: "Home",
106
- });
107
- expect(await store.getmap(404)).toBeUndefined();
108
- });
109
-
110
- it("keeps the key hold registry with tab and step provenance across writes", async () => {
111
- const store = new sessionmemory(new fakeadapter());
112
- expect(await store.getholds()).toEqual([]);
113
- await store.setholds([
114
- { holdid: "shift1", key: "Shift", modifiers: ["shift"], tabid: 4, stepid: "s1", pressedat: 100 },
115
- { holdid: "ctrl1", key: "Control", tabid: 4, stepid: "s2", pressedat: 200, releasedat: 260 },
116
- ]);
117
- const holds = await store.getholds();
118
- expect(holds).toHaveLength(2);
119
- expect(holds[0]).toMatchObject({ holdid: "shift1", key: "Shift", tabid: 4, stepid: "s1", pressedat: 100 });
120
- expect(holds[1]?.releasedat).toBe(260);
121
- });
122
-
123
- it("records dialog decisions with the reviewed answer for the audit trail", async () => {
124
- const store = new sessionmemory(new fakeadapter());
125
- await store.adddialog({
126
- id: "d1",
127
- dialog: "confirm",
128
- text: "Leave the page?",
129
- accept: true,
130
- sessionid: "session",
131
- at: 10,
132
- });
133
- await store.adddialog({
134
- id: "d2",
135
- dialog: "prompt",
136
- text: "Your name?",
137
- accept: true,
138
- answer: "devthink",
139
- sessionid: "session",
140
- at: 20,
141
- });
142
- const dialogs = await store.getdialogs();
143
- expect(dialogs.map((decision) => decision.id)).toEqual(["d2", "d1"]);
144
- expect(dialogs[0]).toMatchObject({ dialog: "prompt", accept: true, answer: "devthink" });
145
- expect(dialogs[1]?.accept).toBe(true);
146
- });
147
-
148
- it("records retry outcomes with attempts and movement deltas", async () => {
149
- const store = new sessionmemory(new fakeadapter());
150
- await store.addretry({ stepid: "wrap", attempts: 3, movement: 12.5, ok: true, at: 30 });
151
- await store.addretry({ stepid: "wrap", attempts: 1, movement: 0, ok: false, at: 40 });
152
- const retries = await store.getretries();
153
- expect(retries.map((outcome) => outcome.attempts)).toEqual([1, 3]);
154
- expect(retries[1]).toMatchObject({ stepid: "wrap", movement: 12.5, ok: true });
155
- });
156
-
157
- it("records resolution summaries per target mode for later selector derivation", async () => {
158
- const store = new sessionmemory(new fakeadapter());
159
- await store.addresolution({ stepid: "t1", mode: "text", selector: "#signin", label: "Sign in", at: 50 });
160
- await store.addresolution({ stepid: "a1", mode: "aria", selector: "#submit", label: "Submit", at: 60 });
161
- const resolutions = await store.getresolutions();
162
- expect(resolutions.map((summary) => summary.mode)).toEqual(["aria", "text"]);
163
- expect(resolutions[1]).toMatchObject({ stepid: "t1", selector: "#signin", label: "Sign in" });
164
- });
165
-
166
- it("keeps the reviewed default dialog policy for the session auto handler", async () => {
167
- const store = new sessionmemory(new fakeadapter());
168
- expect(await store.getdialogpolicy()).toBeUndefined();
169
- await store.setdialogpolicy({ accept: true, answer: "devthink" });
170
- expect(await store.getdialogpolicy()).toEqual({ accept: true, answer: "devthink" });
171
- await store.setdialogpolicy({ accept: false });
172
- expect(await store.getdialogpolicy()).toEqual({ accept: false });
173
- });
174
-
175
- it("stores every observation version with its schema fields", async () => {
176
- const store = new sessionmemory(new fakeadapter());
177
- const version = await store.nextobservationversion();
178
- await store.setobservation({
179
- version,
180
- observation: {
181
- schemaversion: 3,
182
- url: "https://example.com",
183
- title: "Example",
184
- textpreview: "text",
185
- textlength: 4,
186
- forms: [],
187
- interactive: [],
188
- capturedat: 9,
189
- mode: "passive",
190
- },
191
- });
192
- const record = await store.getobservation(version);
193
- expect(record?.version).toBe(1);
194
- expect(record?.observation.schemaversion).toBe(3);
195
- expect(record?.observation.mode).toBe("passive");
196
- expect(await store.getobservation(404)).toBeUndefined();
197
- });
198
-
199
- it("stores a11y trees and reader articles with the user configured retention", async () => {
200
- const store = new sessionmemory(new fakeadapter());
201
- expect(await store.geta11ytrees()).toEqual([]);
202
- expect(await store.getreaderarticles()).toEqual([]);
203
- const tree = { role: "document", name: "", states: [] as string[], childcount: 0, children: [] };
204
- const article = {
205
- title: "Deep observation",
206
- byline: "Ada",
207
- blocks: [{ kind: "p", text: "text", words: 1 }],
208
- words: 1,
209
- characters: 4,
210
- };
211
- await store.adda11ytree({ version: 1, tree, capturedat: 10 });
212
- await store.adda11ytree({ version: 2, tree, capturedat: 20 });
213
- await store.adda11ytree({ version: 3, tree, capturedat: 30 });
214
- await store.addreaderarticle({ version: 1, article, capturedat: 10 });
215
- await store.addreaderarticle({ version: 2, article, capturedat: 20 });
216
- expect((await store.geta11ytrees()).map((capture) => capture.version)).toEqual([3, 2, 1]);
217
- expect((await store.getreaderarticles()).map((capture) => capture.version)).toEqual([2, 1]);
218
- await store.setsettings({ observationretention: 1 });
219
- await store.adda11ytree({ version: 4, tree, capturedat: 40 });
220
- await store.addreaderarticle({ version: 3, article, capturedat: 40 });
221
- expect((await store.geta11ytrees()).map((capture) => capture.version)).toEqual([4]);
222
- expect((await store.getreaderarticles()).map((capture) => capture.version)).toEqual([3]);
223
- });
224
-
225
- it("stores mutation, focus and banner event streams per session", async () => {
226
- const store = new sessionmemory(new fakeadapter());
227
- expect(await store.getmutationevents()).toEqual([]);
228
- expect(await store.getfocusevents()).toEqual([]);
229
- expect(await store.getbanners()).toEqual([]);
230
- await store.addmutationevent({
231
- watchid: "w1",
232
- event: "childList",
233
- targetpath: "#feed > li",
234
- sessionid: "session",
235
- at: 100,
236
- });
237
- await store.addmutationevent({
238
- watchid: "w1",
239
- event: "attributes",
240
- targetpath: "#feed",
241
- sessionid: "session",
242
- at: 110,
243
- });
244
- await store.addfocusevent({ watchid: "w2", kind: "focus", targetpath: "#search", sessionid: "session", at: 150 });
245
- await store.addfocusevent({ watchid: "w2", kind: "blur", targetpath: "#search", sessionid: "session", at: 160 });
246
- await store.addbanner({
247
- kind: "cookie",
248
- selector: "#cookiebar",
249
- text: "We use cookies.",
250
- controls: ["Accept"],
251
- sessionid: "session",
252
- at: 200,
253
- });
254
- expect((await store.getmutationevents()).map((event) => event.event)).toEqual(["attributes", "childList"]);
255
- expect((await store.getfocusevents()).map((event) => event.kind)).toEqual(["blur", "focus"]);
256
- expect((await store.getbanners())[0]).toMatchObject({ kind: "cookie", controls: ["Accept"], sessionid: "session" });
257
- });
258
-
259
- it("stores snapshot diffs with their two observation versions and derived selectors with stability scores", async () => {
260
- const store = new sessionmemory(new fakeadapter());
261
- expect(await store.getdiffs()).toEqual([]);
262
- expect(await store.getselectors()).toEqual([]);
263
- await store.adddiff({
264
- baseversion: 2,
265
- targetversion: 5,
266
- added: [{ kind: "added", selector: "#new", summary: "New" }],
267
- removed: [],
268
- changed: [{ kind: "changed", selector: "#go", summary: "Go became Stop" }],
269
- at: 42,
270
- });
271
- await store.addselector({ stepid: "s1", selector: "#checkout", strategy: "id", score: 100, at: 50 });
272
- await store.addselector({
273
- stepid: "s2",
274
- selector: 'button[data-testid="checkout"]',
275
- strategy: "attribute",
276
- score: 80,
277
- at: 60,
278
- });
279
- const diffs = await store.getdiffs();
280
- expect(diffs[0]).toMatchObject({ baseversion: 2, targetversion: 5 });
281
- expect(diffs[0]?.added).toHaveLength(1);
282
- expect(diffs[0]?.changed).toHaveLength(1);
283
- const selectors = await store.getselectors();
284
- expect(selectors.map((record) => record.score)).toEqual([80, 100]);
285
- expect(selectors[1]).toMatchObject({ stepid: "s1", selector: "#checkout", strategy: "id", score: 100 });
286
- });
287
-
288
- it("stores detected templates and section fingerprints per origin", async () => {
289
- const store = new sessionmemory(new fakeadapter());
290
- expect(await store.gettemplates()).toEqual([]);
291
- await store.addtemplate({ origin: "https://example.com", template: "article", fingerprint: "fp1a", at: 10 });
292
- await store.addtemplate({
293
- origin: "https://example.com",
294
- template: "",
295
- fingerprint: "fp2b",
296
- section: "#prices",
297
- at: 20,
298
- });
299
- const templates = await store.gettemplates();
300
- expect(templates[0]).toMatchObject({ fingerprint: "fp2b", section: "#prices" });
301
- expect(templates[1]).toMatchObject({ template: "article", fingerprint: "fp1a" });
302
- });
303
-
304
- it("keeps watch registrations across restarts and closes them by watch id", async () => {
305
- const store = new sessionmemory(new fakeadapter());
306
- expect(await store.getwatches()).toEqual([]);
307
- const watch = {
308
- watchid: "w1",
309
- kind: "watchmutate" as const,
310
- stepid: "s1",
311
- sessionid: "session",
312
- origin: "https://example.com",
313
- scopes: ["#feed"],
314
- events: ["childList"],
315
- startedat: 1000,
316
- lifetime: 2500,
317
- };
318
- await store.addwatch(watch);
319
- await store.addwatch({ ...watch, watchid: "w2", kind: "watchfocus", lifetime: 60000 });
320
- await store.closewatch("w1", 3600);
321
- const watches = await store.getwatches();
322
- expect(watches.map((registration) => registration.watchid)).toEqual(["w2", "w1"]);
323
- expect(watches[1]?.closedat).toBe(3600);
324
- expect(watches[0]?.closedat).toBeUndefined();
325
- await store.closewatch("w1", 9999);
326
- expect((await store.getwatches())[1]?.closedat).toBe(3600);
327
- await store.closewatch("w2", 9999);
328
- expect((await store.getwatches())[0]?.closedat).toBe(9999);
329
- });
330
-
331
- it("replaces the live page signals after observation steps", async () => {
332
- const store = new sessionmemory(new fakeadapter());
333
- expect(await store.getsignals()).toBeUndefined();
334
- await store.setsignals({ language: "pt-BR", template: "article", refreshedat: 10 });
335
- expect(await store.getsignals()).toEqual({ language: "pt-BR", template: "article", refreshedat: 10 });
336
- await store.setsignals({
337
- language: "pt-BR",
338
- template: "article",
339
- scrolllocked: true,
340
- banner: "cookie",
341
- refreshedat: 20,
342
- });
343
- expect(await store.getsignals()).toMatchObject({ scrolllocked: true, banner: "cookie", refreshedat: 20 });
344
- });
345
-
346
- it("keeps the navigation trail of a session with timestamps and step refs", async () => {
347
- const store = new sessionmemory(new fakeadapter());
348
- expect(await store.gettrail("session")).toEqual([]);
349
- await store.addtrailentry("session", { url: "https://example.com/", title: "Home", stepid: "s1", at: 10 });
350
- await store.addtrailentry("session", {
351
- url: "https://example.com/pricing",
352
- title: "Pricing",
353
- stepid: "s2",
354
- at: 20,
355
- });
356
- await store.addtrailentry("other", { url: "https://other.example/", title: "Other", at: 30 });
357
- const trail = await store.gettrail("session");
358
- expect(trail).toHaveLength(2);
359
- expect(trail[1]).toMatchObject({ url: "https://example.com/pricing", title: "Pricing", stepid: "s2", at: 20 });
360
- expect(await store.gettrail("other")).toHaveLength(1);
361
- });
362
-
363
- it("stores wait profiles per origin with user configured values", async () => {
364
- const store = new sessionmemory(new fakeadapter());
365
- expect(await store.getwaitprofiles()).toEqual([]);
366
- await store.setwaitprofile({ origin: "https://example.com", profile: { signals: ["load"], idle: 400 }, at: 10 });
367
- await store.setwaitprofile({
368
- origin: "https://example.com",
369
- profile: { signals: ["load", "networkidle"], idle: 900, timeout: 8000 },
370
- at: 20,
371
- });
372
- await store.setwaitprofile({ origin: "https://slow.example", profile: { signals: ["load"], idle: 2000 }, at: 30 });
373
- const profiles = await store.getwaitprofiles();
374
- expect(profiles).toHaveLength(2);
375
- expect(profiles.find((record) => record.origin === "https://example.com")?.profile).toMatchObject({
376
- idle: 900,
377
- timeout: 8000,
378
- });
379
- expect(profiles.find((record) => record.origin === "https://slow.example")?.profile.signals).toEqual(["load"]);
380
- });
381
-
382
- it("stores redirect chains and final urls per navigation step", async () => {
383
- const store = new sessionmemory(new fakeadapter());
384
- expect(await store.getnavrecords()).toEqual([]);
385
- const chain = {
386
- hops: [
387
- { url: "https://example.com/a", status: 302, at: 10 },
388
- { url: "https://example.com/b", status: 200, at: 25 },
389
- ],
390
- startedat: 10,
391
- endedat: 25,
392
- };
393
- await store.addnavrecord({
394
- stepid: "s1",
395
- sessionid: "session",
396
- origin: "https://example.com",
397
- finalurl: "https://example.com/b",
398
- chain,
399
- at: 30,
400
- });
401
- const records = await store.getnavrecords();
402
- expect(records).toHaveLength(1);
403
- expect(records[0]).toMatchObject({ stepid: "s1", finalurl: "https://example.com/b" });
404
- expect(records[0]?.chain.hops[0]).toMatchObject({ status: 302, at: 10 });
405
- await store.setnavstate(4, records[0]!);
406
- expect((await store.getnavstate(4))?.stepid).toBe("s1");
407
- });
408
-
409
- it("stores navintent records, rate limit windows, curated lists and safety verdicts", async () => {
410
- const store = new sessionmemory(new fakeadapter());
411
- await store.addnavintent({
412
- id: "i1",
413
- intent: "open the pricing page",
414
- origin: "https://example.com",
415
- sessionid: "session",
416
- stepid: "s1",
417
- at: 10,
418
- });
419
- expect((await store.getnavintents())[0]).toMatchObject({
420
- intent: "open the pricing page",
421
- origin: "https://example.com",
422
- });
423
- await store.setratestate({
424
- domain: "example.com",
425
- limit: { domain: "example.com", window: 60000, ceiling: 3 },
426
- openedat: 100,
427
- count: 1,
428
- });
429
- await store.setratestate({
430
- domain: "example.com",
431
- limit: { domain: "example.com", window: 60000, ceiling: 5 },
432
- openedat: 200,
433
- count: 0,
434
- });
435
- await store.setratestate({
436
- domain: "other.example",
437
- limit: { domain: "other.example", window: 30000, ceiling: 2 },
438
- openedat: 300,
439
- count: 0,
440
- });
441
- const rates = await store.getratestates();
442
- expect(rates).toHaveLength(2);
443
- expect(rates.find((state) => state.domain === "example.com")).toMatchObject({ count: 0, openedat: 200 });
444
- await store.addcurated({
445
- id: "c1",
446
- links: [
447
- { url: "https://example.com/a", verdict: "safe", reasons: [] },
448
- { url: "http://bad.example", verdict: "unsafe", reasons: ["the url must use HTTPS"] },
449
- ],
450
- at: 10,
451
- });
452
- const curated = await store.getcurateds();
453
- expect(curated[0]?.links[1]).toMatchObject({ verdict: "unsafe" });
454
- await store.addcurated({ ...curated[0]!, reviewedat: 20 });
455
- expect((await store.getcurateds())[0]?.reviewedat).toBe(20);
456
- await store.addsafety({ url: "https://partner.example", safe: true, reasons: [], at: 40 });
457
- expect((await store.getsafeties())[0]).toMatchObject({ url: "https://partner.example", safe: true });
458
- });
459
-
460
- it("stores reviewed auth records, artifacts, nav control, recent tabs and queue counts", async () => {
461
- const store = new sessionmemory(new fakeadapter());
462
- await store.setauth({ origin: "https://example.com", username: "devthink", password: "secret", reviewedat: 10 });
463
- await store.setauth({ origin: "https://example.com", username: "reviewed", password: "changed", reviewedat: 20 });
464
- await store.setauth({ origin: "https://other.example", username: "other", password: "pass", reviewedat: 30 });
465
- const auths = await store.getauths();
466
- expect(auths).toHaveLength(2);
467
- expect(auths.find((record) => record.origin === "https://example.com")?.username).toBe("reviewed");
468
- await store.addartifact({ id: "a1", kind: "printpdf", name: "report.pdf", stepid: "s1", at: 40 });
469
- expect((await store.getartifacts())[0]).toMatchObject({ kind: "printpdf", name: "report.pdf" });
470
- expect(await store.getnavcontrol()).toBeUndefined();
471
- await store.setnavcontrol({ pausedat: 50, reason: "a consent banner is open", updatedat: 50 });
472
- expect(await store.getnavcontrol()).toMatchObject({ pausedat: 50 });
473
- await store.setnavcontrol({ updatedat: 60 });
474
- expect((await store.getnavcontrol())?.pausedat).toBeUndefined();
475
- await store.addrecenttab({ url: "https://example.com/closed", tabid: 4, closedat: 70 });
476
- expect((await store.getrecenttabs())[0]).toMatchObject({ url: "https://example.com/closed", tabid: 4 });
477
- await store.setnavqueues({ prefetch: 2, batchopen: 1, updatedat: 80 });
478
- expect(await store.getnavqueues()).toMatchObject({ prefetch: 2, batchopen: 1 });
479
- });
480
- });
481
-
482
- describe("sessionmemory tabs and windows command", () => {
483
- it("stores named tab layouts with their timestamps and window bounds", async () => {
484
- const store = new sessionmemory(new fakeadapter());
485
- const layout = {
486
- name: "work",
487
- tabs: [{ url: "https://example.com/a", title: "A", pinned: true, index: 0, windowid: 10 }],
488
- groups: [],
489
- windows: [
490
- {
491
- windowid: 10,
492
- state: {
493
- bounds: { left: 0, top: 0, width: 1280, height: 800 },
494
- maximized: false,
495
- profile: "normal" as const,
496
- },
497
- },
498
- ],
499
- savedat: 10,
500
- };
501
- await store.setlayout(layout);
502
- await store.setlayout({ ...layout, savedat: 20 });
503
- await store.setlayout({ ...layout, name: "research", savedat: 30 });
504
- expect((await store.getlayout("work"))?.savedat).toBe(20);
505
- expect((await store.getlayouts()).map((item) => item.name)).toEqual(["research", "work"]);
506
- expect(await store.getlayout("missing")).toBeUndefined();
507
- });
508
-
509
- it("stores tabgroup definitions with their color choices and member tabs", async () => {
510
- const store = new sessionmemory(new fakeadapter());
511
- await store.settabgroup({
512
- groupid: "g1",
513
- name: "research",
514
- color: "blue",
515
- tabids: [4, 5],
516
- collapsed: false,
517
- savedat: 10,
518
- });
519
- await store.settabgroup({
520
- groupid: "g2",
521
- name: "research",
522
- color: "pink",
523
- tabids: [4],
524
- collapsed: true,
525
- savedat: 20,
526
- });
527
- await store.settabgroup({ groupid: "g3", name: "docs", color: "grey", tabids: [6], collapsed: false, savedat: 30 });
528
- const groups = await store.gettabgroups();
529
- expect(groups).toHaveLength(2);
530
- expect(groups.find((group) => group.name === "research")).toMatchObject({
531
- color: "pink",
532
- collapsed: true,
533
- tabids: [4],
534
- });
535
- expect(groups.find((group) => group.name === "docs")?.tabids).toEqual([6]);
536
- });
537
-
538
- it("stores tabmeta records with task provenance per tab", async () => {
539
- const store = new sessionmemory(new fakeadapter());
540
- await store.settabmeta({ tabid: 4, taskrefs: ["plan-1"], provenance: "plan step", labels: ["research"], at: 10 });
541
- await store.settabmeta({ tabid: 4, taskrefs: ["plan-2"], provenance: "user review", labels: ["final"], at: 20 });
542
- await store.settabmeta({ tabid: 5, taskrefs: [], provenance: "plan step", labels: [], at: 30 });
543
- const metas = await store.gettabmetas();
544
- expect(metas).toHaveLength(2);
545
- expect(metas.find((meta) => meta.tabid === 4)).toMatchObject({
546
- taskrefs: ["plan-2"],
547
- labels: ["final"],
548
- provenance: "user review",
549
- });
550
- });
551
-
552
- it("stores session snapshots and the closed tab history for later restore", async () => {
553
- const store = new sessionmemory(new fakeadapter());
554
- const layout = {
555
- name: "run",
556
- tabs: [{ url: "https://example.com/a", title: "A", pinned: false, index: 0, windowid: 10 }],
557
- groups: [],
558
- windows: [],
559
- savedat: 10,
560
- };
561
- await store.addsnapshot({ id: "s1", sessionid: "run-1", layout, capturedat: 10 });
562
- await store.addsnapshot({ id: "s2", sessionid: "run-2", layout, capturedat: 20 });
563
- const snapshots = await store.getsnapshots();
564
- expect(snapshots.map((snapshot) => snapshot.id)).toEqual(["s2", "s1"]);
565
- expect(snapshots[1]).toMatchObject({ sessionid: "run-1", capturedat: 10 });
566
- await store.addclosedtab({
567
- url: "https://example.com/older",
568
- title: "Older",
569
- tabid: 8,
570
- windowid: 10,
571
- closedat: 25,
572
- });
573
- await store.addclosedtab({
574
- url: "https://example.com/closed",
575
- title: "Closed",
576
- tabid: 7,
577
- windowid: 10,
578
- closedat: 30,
579
- });
580
- expect((await store.getclosedtabs())[0]).toMatchObject({ url: "https://example.com/closed", tabid: 7 });
581
- expect((await store.getclosedtabs())[1]?.url).toBe("https://example.com/older");
582
- });
583
-
584
- it("stores badge states per task and the watchtab event stream", async () => {
585
- const store = new sessionmemory(new fakeadapter());
586
- await store.setbadge({ tabid: 4, taskid: "plan", label: "2/5", setat: 10 });
587
- await store.setbadge({ tabid: 5, taskid: "plan", label: "done", setat: 20 });
588
- await store.setbadge({ tabid: 4, taskid: "plan", label: "done", setat: 30 });
589
- const badges = await store.getbadges();
590
- expect(badges).toHaveLength(1);
591
- expect(badges[0]).toMatchObject({ tabid: 4, taskid: "plan", label: "done" });
592
- await store.addtabwatchevent({ watchid: "w1", event: "title", tabid: 4, detail: "New title", at: 40 });
593
- await store.addtabwatchevent({ watchid: "w1", event: "closed", tabid: 5, at: 41 });
594
- const events = await store.gettabwatchevents();
595
- expect(events).toHaveLength(2);
596
- expect(events[0]).toMatchObject({ watchid: "w1", event: "closed", tabid: 5 });
597
- });
598
-
599
- it("stores scratch window ids and the pinned control tab state", async () => {
600
- const store = new sessionmemory(new fakeadapter());
601
- await store.setscratchwindows([11]);
602
- await store.setscratchwindows([11, 12]);
603
- expect(await store.getscratchwindows()).toEqual([11, 12]);
604
- await store.setcontroltab({ tabid: 9, enabled: true, updatedat: 10 });
605
- expect(await store.getcontroltab()).toMatchObject({ tabid: 9, enabled: true });
606
- await store.setcontroltab({ tabid: 0, enabled: false, updatedat: 20 });
607
- expect((await store.getcontroltab())?.enabled).toBe(false);
608
- });
609
-
610
- it("stores form profiles with origin grants, replaces them by name and removes them", async () => {
611
- const store = new sessionmemory(new fakeadapter());
612
- const profile = {
613
- name: "personal",
614
- fields: [{ match: { mode: "label" as const, label: "Full name" }, kind: "text" as const, value: "Ana Alves" }],
615
- grants: ["https://example.com"],
616
- savedat: 10,
617
- };
618
- await store.setprofile(profile);
619
- await store.setprofile({ ...profile, name: "work", savedat: 20 });
620
- expect(await store.getprofile("work")).toMatchObject({ name: "work", savedat: 20 });
621
- expect((await store.getprofiles()).map((item) => item.name)).toEqual(["work", "personal"]);
622
- await store.setprofile({
623
- ...profile,
624
- fields: [
625
- ...profile.fields,
626
- { match: { mode: "name" as const, name: "email" }, kind: "email" as const, value: "a@b.c" },
627
- ],
628
- savedat: 30,
629
- });
630
- expect(await store.getprofiles()).toHaveLength(2);
631
- expect((await store.getprofile("personal"))?.fields).toHaveLength(2);
632
- await store.removeprofile("personal");
633
- expect(await store.getprofile("personal")).toBeUndefined();
634
- expect((await store.getprofiles()).map((item) => item.name)).toEqual(["work"]);
635
- });
636
-
637
- it("stores wizard states, submission tickets, error reports, picks, captcha handoffs, detections and the reviewed code", async () => {
638
- const store = new sessionmemory(new fakeadapter());
639
- await store.addwizard({ index: 1, steps: 3, completed: [true], at: 1 });
640
- await store.addwizard({ index: 2, steps: 3, completed: [true, true], at: 2 });
641
- expect((await store.getwizards())[0]).toMatchObject({ index: 2, steps: 3 });
642
- await store.setticket({ id: "t1", form: "#checkout", valueshash: "abc", consentref: "ask", at: 3 });
643
- await store.setticket({ id: "t1", form: "#checkout", valueshash: "abc", consentref: "ask", approved: true, at: 3 });
644
- expect(await store.gettickets()).toHaveLength(1);
645
- expect((await store.gettickets())[0]?.approved).toBe(true);
646
- await store.adderrorreport({ form: "#checkout", errors: [{ field: "#email", message: "Invalid email." }], at: 4 });
647
- expect((await store.geterrorreports())[0]?.errors).toHaveLength(1);
648
- await store.addpick({ field: "#city", query: "sao", pick: "São Paulo", at: 5 });
649
- expect((await store.getpicks())[0]?.pick).toBe("São Paulo");
650
- await store.addcaptcha({ id: "c1", origin: "https://example.com", resolved: false, openedat: 6 });
651
- await store.resolvecaptcha("c1", 7);
652
- expect((await store.getcaptchas())[0]).toMatchObject({ resolved: true, resolvedat: 7 });
653
- await store.adddetection({ origin: "https://example.com", kind: "login", markers: ["password field"], at: 8 });
654
- expect((await store.getdetections())[0]?.kind).toBe("login");
655
- await store.setcodevalue("123456");
656
- expect(await store.getcodevalue()).toBe("123456");
657
- });
658
-
659
- it("stores datasets, imports, extract sessions, streams, provenance, task rules and sheet endpoints", async () => {
660
- const store = new sessionmemory(new fakeadapter());
661
- const columns = [{ key: "sku", label: "Sku", kind: "text" as const, normalized: "sku" }];
662
- const rows = [{ sku: "a" }, { sku: "b" }];
663
- await store.setdataset({ id: "d2", name: "second", columns, rows, sources: [], at: 2 });
664
- await store.setdataset({
665
- id: "d1",
666
- name: "catalog",
667
- columns,
668
- rows,
669
- sources: [{ row: 0, url: "https://example.com", at: 1, stepid: "s1" }],
670
- at: 3,
671
- });
672
- expect((await store.getdataset("d1"))?.name).toBe("catalog");
673
- expect((await store.getdatasets()).map((item) => item.id)).toEqual(["d1", "d2"]);
674
- await store.addimport({ id: "i1", name: "leads", columns, rows, sources: [], at: 4 });
675
- expect((await store.getimports()).map((item) => item.id)).toEqual(["i1"]);
676
- expect((await store.getdataset("i1"))?.name).toBe("leads");
677
- await store.setextractsession({
678
- id: "e1",
679
- datasetid: "d1",
680
- name: "catalog",
681
- target: "table",
682
- next: "a.next",
683
- planned: 3,
684
- pages: ["https://example.com/1"],
685
- rows: 10,
686
- cursor: 1,
687
- startedat: 5,
688
- updatedat: 6,
689
- });
690
- expect((await store.getextractsessions())[0]).toMatchObject({ id: "e1", cursor: 1, rows: 10, planned: 3 });
691
- await store.setstream({ datasetid: "d1", name: "catalog", chunk: 1, chunks: 3, written: 4, at: 7 });
692
- await store.setstream({ datasetid: "d1", name: "catalog", chunk: 2, chunks: 3, written: 8, at: 8 });
693
- expect((await store.getstreams())[0]).toMatchObject({ chunk: 2, written: 8 });
694
- await store.addprovenance({
695
- artifact: "a1",
696
- name: "catalog.csv",
697
- url: "https://example.com",
698
- stepid: "s2",
699
- rowstart: 1,
700
- rowend: 2,
701
- checksum: "fnv1a-1",
702
- at: 9,
703
- });
704
- expect((await store.getprovenances())[0]).toMatchObject({ artifact: "a1", rowend: 2 });
705
- await store.settaskrules({
706
- taskid: "plan-1",
707
- transforms: [{ expression: "trim", sources: ["sku"], target: "sku" }],
708
- dedupekeys: [],
709
- at: 10,
710
- });
711
- await store.settaskrules({
712
- taskid: "plan-1",
713
- transforms: [{ expression: "trim", sources: ["sku"], target: "sku" }],
714
- dedupekeys: ["sku"],
715
- at: 11,
716
- });
717
- const rules = (await store.gettaskrules())[0]!;
718
- expect(rules.dedupekeys).toEqual(["sku"]);
719
- expect(rules.transforms).toHaveLength(1);
720
- await store.settaskrules({ taskid: "plan-2", transforms: [], dedupekeys: ["price"], at: 12 });
721
- expect(await store.gettaskrules()).toHaveLength(2);
722
- await store.setsheetendpoint({
723
- endpoint: "https://sheets.example/tab",
724
- origin: "https://sheets.example",
725
- configuredat: 12,
726
- });
727
- expect((await store.getsheetendpoints())[0]?.origin).toBe("https://sheets.example");
728
- });
729
-
730
- it("applies artifact retention to exported artifacts and keeps everything without the setting", async () => {
731
- const openstore = new sessionmemory(new fakeadapter());
732
- await openstore.setsettings({});
733
- await openstore.addexport({
734
- id: "a1",
735
- kind: "csv",
736
- name: "one.csv",
737
- stepid: "s1",
738
- rowcount: 1,
739
- content: "x",
740
- checksum: "c1",
741
- at: 1,
742
- });
743
- await openstore.addexport({
744
- id: "a2",
745
- kind: "json",
746
- name: "two.json",
747
- stepid: "s1",
748
- rowcount: 2,
749
- content: "y",
750
- checksum: "c2",
751
- at: 2,
752
- });
753
- expect((await openstore.getexports()).map((item) => item.id)).toEqual(["a2", "a1"]);
754
- await openstore.addexport({
755
- id: "a3",
756
- kind: "excel",
757
- name: "three.xml",
758
- stepid: "s1",
759
- rowcount: 3,
760
- content: "z",
761
- checksum: "c3",
762
- at: 3,
763
- });
764
- expect(await openstore.getexports()).toHaveLength(3);
765
- const bounded = new sessionmemory(new fakeadapter());
766
- await bounded.setsettings({ artifactretention: 2 });
767
- await bounded.addexport({
768
- id: "a1",
769
- kind: "csv",
770
- name: "one.csv",
771
- stepid: "s1",
772
- rowcount: 1,
773
- content: "x",
774
- checksum: "c1",
775
- at: 1,
776
- });
777
- await bounded.addexport({
778
- id: "a2",
779
- kind: "csv",
780
- name: "two.csv",
781
- stepid: "s1",
782
- rowcount: 2,
783
- content: "y",
784
- checksum: "c2",
785
- at: 2,
786
- });
787
- await bounded.addexport({
788
- id: "a3",
789
- kind: "csv",
790
- name: "three.csv",
791
- stepid: "s1",
792
- rowcount: 3,
793
- content: "z",
794
- checksum: "c3",
795
- at: 3,
796
- });
797
- expect((await bounded.getexports()).map((item) => item.id)).toEqual(["a3", "a2"]);
798
- });
799
- });
800
-
801
- describe("sessionmemory files, clipboard and downloads", () => {
802
- it("stores downloads, netlogs with retention, clip consents, clips and quarantines", async () => {
803
- const store = new sessionmemory(new fakeadapter());
804
- await store.setdownload({
805
- id: "d1",
806
- url: "https://example.com/a.pdf",
807
- filename: "a.pdf",
808
- state: "running",
809
- downloadid: 3,
810
- at: 1,
811
- updatedat: 1,
812
- });
813
- await store.setdownload({
814
- id: "d2",
815
- url: "https://example.com/b.zip",
816
- filename: "b.zip",
817
- state: "complete",
818
- path: "/downloads/b.zip",
819
- bytes: 10,
820
- checksum: "fnv1a-1",
821
- at: 2,
822
- updatedat: 3,
823
- });
824
- await store.setdownload({
825
- id: "d1",
826
- url: "https://example.com/a.pdf",
827
- filename: "a.pdf",
828
- state: "paused",
829
- downloadid: 3,
830
- at: 1,
831
- updatedat: 4,
832
- });
833
- expect((await store.getdownloads())[0]).toMatchObject({ id: "d1", state: "paused" });
834
- await store.addnetlog({
835
- url: "https://example.com/list",
836
- method: "GET",
837
- status: 200,
838
- timing: 10,
839
- requestid: "nav-1-1",
840
- stepid: "nav-1",
841
- at: 5,
842
- });
843
- await store.addnetlog({
844
- url: "https://example.com/cart",
845
- method: "GET",
846
- status: 200,
847
- timing: 20,
848
- requestid: "nav-1-2",
849
- stepid: "nav-1",
850
- at: 6,
851
- });
852
- expect(await store.getnetlog()).toHaveLength(2);
853
- await store.setclipconsent({
854
- id: "clip-1",
855
- prompt: "Read the tracking number.",
856
- origin: "https://example.com",
857
- stepid: "s1",
858
- at: 7,
859
- });
860
- await store.setclipconsent({
861
- id: "clip-1",
862
- prompt: "Read the tracking number.",
863
- origin: "https://example.com",
864
- stepid: "s1",
865
- approved: true,
866
- at: 7,
867
- });
868
- expect((await store.getclipconsents())[0]).toMatchObject({ id: "clip-1", approved: true });
869
- await store.addclip({
870
- kind: "read",
871
- hash: "fnv1a-9",
872
- length: 16,
873
- origin: "https://example.com",
874
- stepid: "s1",
875
- at: 8,
876
- });
877
- expect((await store.getclips())[0]).toMatchObject({ kind: "read", hash: "fnv1a-9", length: 16 });
878
- await store.setquarantine({
879
- id: "q1",
880
- path: "devthink-quarantine/x.pdf",
881
- reason: "mime matched",
882
- scan: "pending",
883
- at: 9,
884
- updatedat: 9,
885
- });
886
- await store.setquarantine({
887
- id: "q1",
888
- path: "devthink-quarantine/x.pdf",
889
- reason: "mime matched",
890
- scan: "clean",
891
- release: "user-1",
892
- at: 9,
893
- updatedat: 10,
894
- });
895
- expect((await store.getquarantines())[0]).toMatchObject({ scan: "clean", release: "user-1" });
896
- });
897
-
898
- it("applies netlog retention and keeps every record without the setting", async () => {
899
- const openstore = new sessionmemory(new fakeadapter());
900
- await openstore.setsettings({});
901
- await openstore.addnetlog({
902
- url: "https://example.com/1",
903
- method: "GET",
904
- status: 200,
905
- timing: 1,
906
- stepid: "s1",
907
- at: 1,
908
- });
909
- await openstore.addnetlog({
910
- url: "https://example.com/2",
911
- method: "GET",
912
- status: 200,
913
- timing: 2,
914
- stepid: "s1",
915
- at: 2,
916
- });
917
- await openstore.addnetlog({
918
- url: "https://example.com/3",
919
- method: "GET",
920
- status: 200,
921
- timing: 3,
922
- stepid: "s1",
923
- at: 3,
924
- });
925
- expect(await openstore.getnetlog()).toHaveLength(3);
926
- const bounded = new sessionmemory(new fakeadapter());
927
- await bounded.setsettings({ netlogretention: 2 });
928
- await bounded.addnetlog({
929
- url: "https://example.com/1",
930
- method: "GET",
931
- status: 200,
932
- timing: 1,
933
- stepid: "s1",
934
- at: 1,
935
- });
936
- await bounded.addnetlog({
937
- url: "https://example.com/2",
938
- method: "GET",
939
- status: 200,
940
- timing: 2,
941
- stepid: "s1",
942
- at: 2,
943
- });
944
- await bounded.addnetlog({
945
- url: "https://example.com/3",
946
- method: "GET",
947
- status: 200,
948
- timing: 3,
949
- stepid: "s1",
950
- at: 3,
951
- });
952
- expect((await bounded.getnetlog()).map((record) => record.url)).toEqual([
953
- "https://example.com/3",
954
- "https://example.com/2",
955
- ]);
956
- });
957
-
958
- it("stores cleanup rules, run history, capture counters, inventories, scan hooks and mime filters", async () => {
959
- const store = new sessionmemory(new fakeadapter());
960
- await store.setcleanuprules([{ age: 60000, kind: "any", keep: "latest" }]);
961
- expect(await store.getcleanuprules()).toEqual([{ age: 60000, kind: "any", keep: "latest" }]);
962
- await store.addcleanuprun({ id: "r1", rules: 1, removed: 2, kept: 5, at: 1 });
963
- expect((await store.getcleanupruns())[0]).toMatchObject({ removed: 2, kept: 5 });
964
- await store.setcapturecounter({ taskid: "task-1", counters: { snap: 2 }, at: 2 });
965
- await store.setcapturecounter({ taskid: "task-1", counters: { snap: 3 }, at: 3 });
966
- await store.setcapturecounter({ taskid: "task-2", counters: { peek: 1 }, at: 4 });
967
- expect((await store.getcapturecounters()).map((counter) => counter.taskid)).toEqual(["task-2", "task-1"]);
968
- expect((await store.getcapturecounters())[1]?.counters).toEqual({ snap: 3 });
969
- await store.setinventory([{ id: "a1", kind: "export-csv", name: "old.csv", size: 10, at: 5 }]);
970
- expect((await store.getinventory())[0]).toMatchObject({ name: "old.csv", size: 10 });
971
- await store.setscanhook({
972
- scanner: "clamav",
973
- endpoint: "https://scanner.example/verdict",
974
- origin: "https://scanner.example",
975
- configuredat: 6,
976
- });
977
- expect((await store.getscanhooks())[0]).toMatchObject({ scanner: "clamav" });
978
- await store.setscanhook({
979
- scanner: "second",
980
- endpoint: "https://other.example/v",
981
- origin: "https://other.example",
982
- configuredat: 7,
983
- });
984
- await store.setscanhook({
985
- scanner: "clamav",
986
- endpoint: "https://scanner.example/v2",
987
- origin: "https://scanner.example",
988
- configuredat: 8,
989
- });
990
- expect(await store.getscanhooks()).toHaveLength(2);
991
- await store.setmimefilters([{ include: ["application/pdf*"], exclude: [], default: "deny" }]);
992
- expect((await store.getmimefilters())[0]?.default).toBe("deny");
993
- });
994
-
995
- it("removes exported artifacts and run store artifacts for cleanup sweeps", async () => {
996
- const store = new sessionmemory(new fakeadapter());
997
- await store.addexport({
998
- id: "a1",
999
- kind: "csv",
1000
- name: "one.csv",
1001
- stepid: "s1",
1002
- rowcount: 1,
1003
- content: "x",
1004
- checksum: "c1",
1005
- at: 1,
1006
- });
1007
- await store.addexport({
1008
- id: "a2",
1009
- kind: "csv",
1010
- name: "two.csv",
1011
- stepid: "s1",
1012
- rowcount: 2,
1013
- content: "yy",
1014
- checksum: "c2",
1015
- at: 2,
1016
- });
1017
- expect(await store.removeexport("a1")).toBe(true);
1018
- expect(await store.removeexport("missing")).toBe(false);
1019
- expect((await store.getexports()).map((artifact) => artifact.id)).toEqual(["a2"]);
1020
- await store.addartifact({ id: "r1", kind: "printpdf", name: "receipt.pdf", stepid: "s2", at: 3 });
1021
- expect(await store.removeartifact("r1")).toBe(true);
1022
- expect(await store.removeartifact("r1")).toBe(false);
1023
- expect(await store.getartifacts()).toEqual([]);
1024
- });
1025
- });
1026
-
1027
- describe("capture memory", () => {
1028
- it("stores capture records with bytes, filters by run, step and kind, and returns pairs of one run", async () => {
1029
- const store = new sessionmemory(new fakeadapter());
1030
- await store.addcapture({
1031
- id: "c1",
1032
- runid: "run-1",
1033
- stepid: "s1",
1034
- kind: "shotview",
1035
- format: "png",
1036
- width: 1280,
1037
- height: 800,
1038
- capturedat: 1,
1039
- bytes: "data:image/png;base64,aaa",
1040
- });
1041
- await store.addcapture({
1042
- id: "c2",
1043
- runid: "run-1",
1044
- stepid: "s2",
1045
- kind: "shotfullpage",
1046
- format: "png",
1047
- width: 1280,
1048
- height: 4000,
1049
- capturedat: 2,
1050
- bytes: "data:image/png;base64,bbb",
1051
- });
1052
- await store.addcapture({
1053
- id: "c3",
1054
- runid: "run-2",
1055
- stepid: "s1",
1056
- kind: "shotview",
1057
- format: "webp",
1058
- width: 640,
1059
- height: 400,
1060
- capturedat: 3,
1061
- bytes: "data:image/webp;base64,ccc",
1062
- });
1063
- expect((await store.getcaptures()).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
1064
- expect(await store.getcapture("c2")).toMatchObject({ kind: "shotfullpage", bytes: "data:image/png;base64,bbb" });
1065
- expect(await store.getcapture("missing")).toBeUndefined();
1066
- expect((await store.listcaptures({ runid: "run-1" })).map((record) => record.id)).toEqual(["c2", "c1"]);
1067
- expect((await store.listcaptures({ stepid: "s1" })).map((record) => record.id)).toEqual(["c3", "c1"]);
1068
- expect((await store.listcaptures({ runid: "run-1", kind: "shotview" })).map((record) => record.id)).toEqual(["c1"]);
1069
- expect(await store.listcaptures({})).toHaveLength(3);
1070
- await store.addpair({
1071
- id: "p1",
1072
- beforeid: "c1",
1073
- afterid: "c2",
1074
- actionkind: "click",
1075
- target: "#submit",
1076
- domsnapshotid: "7",
1077
- at: 4,
1078
- });
1079
- await store.addpair({ id: "p2", beforeid: "c3", afterid: "c3", actionkind: "type", at: 5 });
1080
- expect((await store.getpairs()).map((pair) => pair.id)).toEqual(["p2", "p1"]);
1081
- expect((await store.getpairs("run-1")).map((pair) => pair.id)).toEqual(["p1"]);
1082
- expect((await store.getpairs("run-2")).map((pair) => pair.id)).toEqual(["p2"]);
1083
- });
1084
-
1085
- it("expires capture bytes after the configured retention window while keeping the metadata", async () => {
1086
- const store = new sessionmemory(new fakeadapter());
1087
- await store.setsettings({ captureretention: 2 });
1088
- await store.addcapture({
1089
- id: "c1",
1090
- runid: "run-1",
1091
- stepid: "s1",
1092
- kind: "shotview",
1093
- format: "png",
1094
- width: 10,
1095
- height: 10,
1096
- capturedat: 1,
1097
- bytes: "data:1",
1098
- });
1099
- await store.addcapture({
1100
- id: "c2",
1101
- runid: "run-1",
1102
- stepid: "s2",
1103
- kind: "shotview",
1104
- format: "png",
1105
- width: 10,
1106
- height: 10,
1107
- capturedat: 2,
1108
- bytes: "data:2",
1109
- });
1110
- await store.addcapture({
1111
- id: "c3",
1112
- runid: "run-1",
1113
- stepid: "s3",
1114
- kind: "shotview",
1115
- format: "png",
1116
- width: 10,
1117
- height: 10,
1118
- capturedat: 3,
1119
- bytes: "data:3",
1120
- });
1121
- const stored = await store.getcaptures();
1122
- expect(stored.map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
1123
- expect(stored[0]).toMatchObject({ bytes: "data:3" });
1124
- expect(stored[1]).toMatchObject({ bytes: "data:2" });
1125
- expect(stored[2]).toMatchObject({ bytesexpired: true });
1126
- expect(stored[2]?.bytes).toBeUndefined();
1127
- expect((await store.listcaptures({ runid: "run-1" })).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
1128
- const unbounded = new sessionmemory(new fakeadapter());
1129
- await unbounded.addcapture({
1130
- id: "c1",
1131
- runid: "run-1",
1132
- stepid: "s1",
1133
- kind: "shotview",
1134
- format: "png",
1135
- width: 10,
1136
- height: 10,
1137
- capturedat: 1,
1138
- bytes: "data:1",
1139
- });
1140
- await unbounded.addcapture({
1141
- id: "c2",
1142
- runid: "run-1",
1143
- stepid: "s2",
1144
- kind: "shotview",
1145
- format: "png",
1146
- width: 10,
1147
- height: 10,
1148
- capturedat: 2,
1149
- bytes: "data:2",
1150
- });
1151
- expect((await unbounded.getcaptures()).every((record) => record.bytes !== undefined)).toBe(true);
1152
- });
1153
- });
1154
-
1155
- describe("media memory", () => {
1156
- it("stores media records per run, filters by run and kind and returns recordings with their file reference", async () => {
1157
- const store = new sessionmemory(new fakeadapter());
1158
- await store.addmedia({
1159
- id: "pdf-1",
1160
- runid: "run-1",
1161
- stepid: "s1",
1162
- pages: 3,
1163
- pagewidth: 612,
1164
- pageheight: 792,
1165
- margins: { top: 28.8, right: 28.8, bottom: 28.8, left: 28.8 },
1166
- landscape: false,
1167
- scale: 1,
1168
- bytes: 4200,
1169
- at: 1,
1170
- dataurl: "data:application/pdf;base64,aaa",
1171
- name: "run-1-s1-1-capturepdf.pdf",
1172
- });
1173
- await store.addmedia({
1174
- id: "rec-1",
1175
- runid: "run-1",
1176
- stepid: "s2",
1177
- tabid: 4,
1178
- kind: "screen",
1179
- scope: "run",
1180
- format: "frames",
1181
- startedat: 2,
1182
- endedat: 7,
1183
- duration: 5,
1184
- fps: 2,
1185
- frames: ["f1", "f2"],
1186
- file: "run-1-s2-2-recordscreen.json",
1187
- bytes: 300,
1188
- at: 2,
1189
- });
1190
- await store.addmedia({
1191
- id: "frame-1",
1192
- runid: "run-2",
1193
- stepid: "s3",
1194
- source: "video.tutorial",
1195
- timestamp: 12.5,
1196
- poster: true,
1197
- format: "png",
1198
- width: 1280,
1199
- height: 720,
1200
- at: 3,
1201
- dataurl: "data:image/png;base64,bbb",
1202
- });
1203
- await store.addmedia({
1204
- id: "canvas-1",
1205
- runid: "run-2",
1206
- stepid: "s4",
1207
- element: "canvas.chart",
1208
- context: "webgl",
1209
- width: 600,
1210
- height: 400,
1211
- format: "png",
1212
- at: 4,
1213
- dataurl: "data:image/png;base64,ccc",
1214
- });
1215
- await store.addmedia({
1216
- id: "stream-1",
1217
- runid: "run-2",
1218
- stepid: "s5",
1219
- kind: "webrtc",
1220
- tracks: 2,
1221
- label: "stream-1",
1222
- live: true,
1223
- at: 5,
1224
- });
1225
- await store.addmedia({
1226
- id: "asset-1",
1227
- runid: "run-2",
1228
- stepid: "s6",
1229
- kind: "favicon",
1230
- url: "https://example.com/icon.png",
1231
- bytes: 800,
1232
- sizes: "32x32",
1233
- at: 6,
1234
- });
1235
- expect((await store.getmediarecords()).map((record) => record.id)).toEqual([
1236
- "asset-1",
1237
- "stream-1",
1238
- "canvas-1",
1239
- "frame-1",
1240
- "rec-1",
1241
- "pdf-1",
1242
- ]);
1243
- expect((await store.listmedia({ runid: "run-1" })).map((record) => record.id)).toEqual(["rec-1", "pdf-1"]);
1244
- expect((await store.listmedia({ kind: "recording" })).map((record) => record.id)).toEqual(["rec-1"]);
1245
- expect((await store.listmedia({ kind: "pdf" })).map((record) => record.id)).toEqual(["pdf-1"]);
1246
- expect((await store.listmedia({ runid: "run-2", kind: "frame" })).map((record) => record.id)).toEqual(["frame-1"]);
1247
- expect(await store.listmedia({})).toHaveLength(6);
1248
- expect(await store.getmediarecord("canvas-1")).toMatchObject({ context: "webgl" });
1249
- expect(await store.getmediarecord("missing")).toBeUndefined();
1250
- expect(await store.getrecording("rec-1")).toMatchObject({
1251
- kind: "screen",
1252
- file: "run-1-s2-2-recordscreen.json",
1253
- frames: ["f1", "f2"],
1254
- duration: 5,
1255
- });
1256
- expect(await store.getrecording("pdf-1")).toBeUndefined();
1257
- await store.removemedia("stream-1");
1258
- expect((await store.getmediarecords()).map((record) => record.id)).not.toContain("stream-1");
1259
- });
1260
-
1261
- it("expires media bytes after the retention window while keeping the recording index and metadata", async () => {
1262
- const store = new sessionmemory(new fakeadapter());
1263
- await store.setsettings({ mediaretention: 2 });
1264
- await store.addmedia({
1265
- id: "pdf-1",
1266
- runid: "run-1",
1267
- stepid: "s1",
1268
- pages: 1,
1269
- pagewidth: 612,
1270
- pageheight: 792,
1271
- margins: { top: 28.8, right: 28.8, bottom: 28.8, left: 28.8 },
1272
- landscape: false,
1273
- scale: 1,
1274
- bytes: 100,
1275
- at: 1,
1276
- dataurl: "data:1",
1277
- });
1278
- await store.addmedia({
1279
- id: "rec-1",
1280
- runid: "run-1",
1281
- stepid: "s2",
1282
- tabid: 4,
1283
- kind: "audio",
1284
- scope: "tab",
1285
- format: "evidence",
1286
- startedat: 2,
1287
- frames: ["e1"],
1288
- file: "rec.json",
1289
- bytes: 50,
1290
- at: 2,
1291
- });
1292
- await store.addmedia({
1293
- id: "frame-1",
1294
- runid: "run-1",
1295
- stepid: "s3",
1296
- source: "video",
1297
- timestamp: 0,
1298
- poster: false,
1299
- format: "png",
1300
- width: 10,
1301
- height: 10,
1302
- at: 3,
1303
- dataurl: "data:3",
1304
- });
1305
- const stored = await store.getmediarecords();
1306
- expect(stored.map((record) => record.id)).toEqual(["frame-1", "rec-1", "pdf-1"]);
1307
- expect(stored[0]).toMatchObject({ dataurl: "data:3" });
1308
- expect(stored[1]).toMatchObject({ bytes: 50 });
1309
- expect(stored[2]).toMatchObject({ bytesexpired: true });
1310
- expect((stored[2] as { dataurl?: string }).dataurl).toBeUndefined();
1311
- await store.addmedia({
1312
- id: "frame-2",
1313
- runid: "run-1",
1314
- stepid: "s4",
1315
- source: "video",
1316
- timestamp: 1,
1317
- poster: false,
1318
- format: "png",
1319
- width: 10,
1320
- height: 10,
1321
- at: 4,
1322
- dataurl: "data:4",
1323
- });
1324
- const after = await store.getmediarecords();
1325
- expect(after.map((record) => record.id)).toEqual(["frame-2", "frame-1", "rec-1", "pdf-1"]);
1326
- expect((after[2] as recordinglike).bytes).toBeUndefined();
1327
- expect((after[2] as recordinglike).bytesexpired).toBe(true);
1328
- expect((after[2] as recordinglike).frames).toEqual(["e1"]);
1329
- expect((after[2] as recordinglike).file).toBe("rec.json");
1330
- const unbounded = new sessionmemory(new fakeadapter());
1331
- await unbounded.addmedia({
1332
- id: "pdf-1",
1333
- runid: "run-1",
1334
- stepid: "s1",
1335
- pages: 1,
1336
- pagewidth: 612,
1337
- pageheight: 792,
1338
- margins: { top: 1, right: 1, bottom: 1, left: 1 },
1339
- landscape: false,
1340
- scale: 1,
1341
- bytes: 100,
1342
- at: 1,
1343
- dataurl: "data:1",
1344
- });
1345
- await unbounded.addmedia({
1346
- id: "rec-1",
1347
- runid: "run-1",
1348
- stepid: "s2",
1349
- tabid: 4,
1350
- kind: "screen",
1351
- scope: "tab",
1352
- format: "frames",
1353
- startedat: 2,
1354
- bytes: 50,
1355
- at: 2,
1356
- });
1357
- expect(
1358
- (await unbounded.getmediarecords()).every(
1359
- (record) => (record as { bytesexpired?: boolean }).bytesexpired === undefined,
1360
- ),
1361
- ).toBe(true);
1362
- });
1363
-
1364
- it("stores image batches with match counts and recording consent decisions per origin", async () => {
1365
- const store = new sessionmemory(new fakeadapter());
1366
- await store.addimagebatch({
1367
- id: "batch-1",
1368
- runid: "run-1",
1369
- stepid: "s1",
1370
- images: [
1371
- { url: "https://example.com/a.png", alt: "Chart", width: 400, height: 300, bytes: 1200, mime: "image/png" },
1372
- ],
1373
- matched: 1,
1374
- downloaded: 1,
1375
- at: 1,
1376
- });
1377
- await store.addimagebatch({
1378
- id: "batch-2",
1379
- runid: "run-1",
1380
- stepid: "s2",
1381
- images: [],
1382
- matched: 0,
1383
- downloaded: 0,
1384
- at: 2,
1385
- });
1386
- await store.addimagebatch({
1387
- id: "batch-1",
1388
- runid: "run-1",
1389
- stepid: "s1",
1390
- images: [{ url: "https://example.com/b.png", alt: "", width: 50, height: 50, bytes: 10, mime: "image/png" }],
1391
- matched: 1,
1392
- downloaded: 0,
1393
- at: 3,
1394
- });
1395
- expect((await store.getimagebatches()).map((batch) => batch.id)).toEqual(["batch-1", "batch-2"]);
1396
- expect((await store.getimagebatches())[0]).toMatchObject({
1397
- matched: 1,
1398
- downloaded: 0,
1399
- images: [{ url: "https://example.com/b.png" }],
1400
- });
1401
- await store.setrecordingconsent({
1402
- id: "consent-1",
1403
- prompt: "Record the checkout flow.",
1404
- origin: "https://example.com",
1405
- stepid: "s3",
1406
- at: 4,
1407
- });
1408
- await store.setrecordingconsent({
1409
- id: "consent-2",
1410
- prompt: "Record the audio.",
1411
- origin: "https://other.example",
1412
- stepid: "s4",
1413
- at: 5,
1414
- });
1415
- await store.setrecordingconsent({
1416
- id: "consent-1",
1417
- prompt: "Record the checkout flow.",
1418
- origin: "https://example.com",
1419
- stepid: "s3",
1420
- approved: true,
1421
- at: 4,
1422
- });
1423
- const consents = await store.getrecordingconsents();
1424
- expect(consents.map((consent) => consent.id)).toEqual(["consent-1", "consent-2"]);
1425
- expect(consents[0]).toMatchObject({ approved: true, origin: "https://example.com" });
1426
- });
1427
- });
1428
-
1429
- type recordinglike = { bytes?: number; bytesexpired?: boolean; frames?: string[]; file?: string };
1430
-
1431
- describe("network observation memory", () => {
1432
- const call = (id: string, origin: string, body?: string): callrecord => ({
1433
- id,
1434
- runid: "run-1",
1435
- stepid: "f1",
1436
- kind: "fetch",
1437
- url: `${origin}/data`,
1438
- origin,
1439
- method: "GET",
1440
- status: 200,
1441
- statusclass: "success",
1442
- duration: 10,
1443
- retries: 0,
1444
- bytes: (body ?? "").length,
1445
- headernames: ["x-request-id"],
1446
- ...(body !== undefined ? { body } : {}),
1447
- at: 1,
1448
- });
1449
-
1450
- it("stores call records with bodies and lists them filtered by run and origin", async () => {
1451
- const store = new sessionmemory(new fakeadapter());
1452
- await store.addcall(call("c1", "https://api.example", '{"a":1}'));
1453
- await store.addcall(call("c2", "https://other.example", '{"b":2}'));
1454
- await store.addcall({ ...call("c3", "https://api.example"), runid: "run-2" });
1455
- expect((await store.getcalls()).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
1456
- expect((await store.listcalls({ runid: "run-1" })).map((record) => record.id)).toEqual(["c2", "c1"]);
1457
- expect((await store.listcalls({ origin: "https://api.example" })).map((record) => record.id)).toEqual(["c3", "c1"]);
1458
- expect((await store.listcalls({})).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
1459
- expect((await store.getcall("c2"))?.body).toBe('{"b":2}');
1460
- });
1461
-
1462
- it("expires call bodies after the configured retention window while the metadata survives", async () => {
1463
- const store = new sessionmemory(new fakeadapter());
1464
- await store.setsettings({ callretention: 2 });
1465
- await store.addcall(call("c1", "https://api.example", "one"));
1466
- await store.addcall(call("c2", "https://api.example", "two"));
1467
- await store.addcall(call("c3", "https://api.example", "three"));
1468
- const records = await store.getcalls();
1469
- expect(records.map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
1470
- expect(records[0]?.body).toBe("three");
1471
- expect(records[1]?.body).toBe("two");
1472
- expect(records[2]?.body).toBeUndefined();
1473
- expect(records[2]?.bodyexpired).toBe(true);
1474
- expect(records[2]?.statusclass).toBe("success");
1475
- });
1476
-
1477
- it("stores endpoint definitions with version history and returns the newest schema", async () => {
1478
- const store = new sessionmemory(new fakeadapter());
1479
- const schema = { fields: [{ name: "owner", kind: "string" as const, required: true }] };
1480
- await store.setendpoint({
1481
- name: "issues",
1482
- method: "GET",
1483
- url: "https://api.example/issues",
1484
- schema,
1485
- version: 1,
1486
- at: 1,
1487
- });
1488
- await store.setendpoint({
1489
- name: "issues",
1490
- method: "GET",
1491
- url: "https://api.example/v2/issues",
1492
- schema,
1493
- version: 1,
1494
- at: 2,
1495
- });
1496
- const newest = await store.getendpoint("issues");
1497
- expect(newest?.url).toBe("https://api.example/v2/issues");
1498
- expect(newest?.version).toBe(2);
1499
- const endpoints = await store.getendpoints();
1500
- expect(endpoints).toHaveLength(1);
1501
- expect(endpoints[0]?.version).toBe(2);
1502
- expect(await store.getendpoint("missing")).toBeUndefined();
1503
- });
1504
-
1505
- it("stores fetch consents per origin and api key references without key material", async () => {
1506
- const store = new sessionmemory(new fakeadapter());
1507
- await store.setfetchconsent({
1508
- id: "consent-1",
1509
- origin: "https://api.example",
1510
- headers: [{ name: "x-api-key", value: "secret" }],
1511
- approved: true,
1512
- expiresat: 99,
1513
- at: 1,
1514
- });
1515
- const consents = await store.getfetchconsents();
1516
- expect(consents[0]?.headers[0]).toEqual({ name: "x-api-key", value: "secret" });
1517
- await store.setapikey({
1518
- name: "github",
1519
- origins: ["https://api.example"],
1520
- header: "authorization",
1521
- storageid: "apikey-github",
1522
- createdat: 1,
1523
- });
1524
- await store.setsecret("apikey-github", "ghp-token");
1525
- expect((await store.getapikeys())[0]).toMatchObject({ name: "github", header: "authorization" });
1526
- expect(await store.getsecret("apikey-github")).toBe("ghp-token");
1527
- await store.removeapikey("github");
1528
- expect(await store.getapikeys()).toHaveLength(0);
1529
- expect(await store.getsecret("apikey-github")).toBeUndefined();
1530
- });
1531
- });
1532
-
1533
- describe("network observation part two memory", () => {
1534
- it("stores channel records with message counters and stream subscriptions", async () => {
1535
- const memory = new sessionmemory(new fakeadapter());
1536
- const channel = {
1537
- id: "ch1",
1538
- runid: "run",
1539
- stepid: "s1",
1540
- kind: "websocket" as const,
1541
- url: "wss://api.example/live",
1542
- origin: "https://api.example",
1543
- state: "open" as const,
1544
- openedat: 1,
1545
- sent: 0,
1546
- received: 0,
1547
- reconnects: 0,
1548
- };
1549
- await memory.addchannel(channel);
1550
- await memory.addchannel({ ...channel, sent: 2, received: 5 });
1551
- expect((await memory.getchannel("ch1"))?.sent).toBe(2);
1552
- expect(await memory.getchannels()).toHaveLength(1);
1553
- const subscription = {
1554
- id: "sub1",
1555
- runid: "run",
1556
- stepid: "s2",
1557
- url: "https://api.example/stream",
1558
- origin: "https://api.example",
1559
- state: "open" as const,
1560
- events: 0,
1561
- names: [] as string[],
1562
- cancel: { kind: "stop" as const, value: "done" },
1563
- openedat: 1,
1564
- };
1565
- await memory.setsubscription(subscription);
1566
- await memory.setsubscription({ ...subscription, events: 9, names: ["userjoin"], lasteventid: "41" });
1567
- expect((await memory.getsubscriptions())[0]).toMatchObject({ events: 9, lasteventid: "41" });
1568
- });
1569
-
1570
- it("queues message envelopes with sequence integrity and drains consumed matches", async () => {
1571
- const memory = new sessionmemory(new fakeadapter());
1572
- await memory.addmessage({ channelid: "ch1", stream: "orders", payload: '{"id":1}', sequence: 1, at: 1 });
1573
- await memory.addmessage({ channelid: "ch1", stream: "orders", payload: '{"id":2}', sequence: 2, at: 2 });
1574
- await memory.addmessage({ channelid: "ch1", stream: "ticks", payload: "{}", sequence: 3, at: 3 });
1575
- expect((await memory.getmessages()).map((envelope) => envelope.sequence)).toEqual([1, 2, 3]);
1576
- expect((await memory.getmessages("ch1", "orders")).map((envelope) => envelope.payload)).toEqual([
1577
- '{"id":1}',
1578
- '{"id":2}',
1579
- ]);
1580
- await memory.addmessage({ channelid: "ch1", stream: "orders", payload: '{"id":1}', sequence: 1, at: 1 });
1581
- expect(await memory.getmessages()).toHaveLength(3);
1582
- await memory.drainmessages([
1583
- { channelid: "ch1", sequence: 1 },
1584
- { channelid: "ch1", sequence: 3 },
1585
- ]);
1586
- expect((await memory.getmessages()).map((envelope) => envelope.sequence)).toEqual([2]);
1587
- });
1588
-
1589
- it("stores exchanges with correlation ids and filters by run, origin and status", async () => {
1590
- const memory = new sessionmemory(new fakeadapter());
1591
- const exchange = {
1592
- id: "e1",
1593
- runid: "run",
1594
- stepid: "s1",
1595
- correlationid: "run-1",
1596
- url: "https://api.example/items",
1597
- origin: "https://api.example",
1598
- method: "GET",
1599
- status: 200,
1600
- statusclass: "success",
1601
- source: "extension" as const,
1602
- timing: 10,
1603
- bytes: 20,
1604
- at: 1,
1605
- };
1606
- await memory.addexchange(exchange);
1607
- await memory.addexchange({
1608
- ...exchange,
1609
- id: "e2",
1610
- correlationid: "run-2",
1611
- url: "https://api.example/gone",
1612
- status: 0,
1613
- statusclass: "unknown",
1614
- errorclass: "networkerror",
1615
- });
1616
- await memory.addexchange({ ...exchange, id: "e3", correlationid: "run-3", runid: "other" });
1617
- expect((await memory.getexchange("e2"))?.errorclass).toBe("networkerror");
1618
- expect(await memory.listexchanges({ runid: "run" })).toHaveLength(2);
1619
- expect(await memory.listexchanges({ runid: "run", origin: "https://api.example" })).toHaveLength(2);
1620
- expect(await memory.listexchanges({ runid: "run", status: 200 })).toHaveLength(1);
1621
- expect(await memory.listexchanges({ runid: "run", status: "failed" })).toHaveLength(1);
1622
- });
1623
-
1624
- it("expires captured bodies after the configured retention window while the metadata survives", async () => {
1625
- const memory = new sessionmemory(new fakeadapter());
1626
- const body = (ref: string, correlationid: string): bodyrecord => ({
1627
- ref,
1628
- runid: "run",
1629
- correlationid,
1630
- url: "https://api.example/items",
1631
- mime: "application/json",
1632
- bytes: 8,
1633
- body: '{"id":1}',
1634
- at: 1,
1635
- });
1636
- await memory.addbody(body("b1", "run-1"));
1637
- await memory.addbody(body("b2", "run-2"));
1638
- expect((await memory.getbody("b1"))?.body).toBe('{"id":1}');
1639
- await memory.setsettings({ bodyretention: 2 });
1640
- await memory.addbody(body("b3", "run-3"));
1641
- expect((await memory.getbodies()).map((record) => record.ref)).toEqual(["b3", "b2", "b1"]);
1642
- expect((await memory.getbodies())[2]?.bodyexpired).toBe(true);
1643
- await memory.addbody(body("b4", "run-4"));
1644
- const stored = await memory.getbodies();
1645
- expect(stored.map((record) => record.ref)).toEqual(["b4", "b3", "b2", "b1"]);
1646
- expect(stored[1]?.body).toBe('{"id":1}');
1647
- expect(stored[1]?.bodyexpired).toBeUndefined();
1648
- expect(stored[2]?.body).toBeUndefined();
1649
- expect(stored[2]?.bodyexpired).toBe(true);
1650
- expect(stored[2]?.bytes).toBe(8);
1651
- expect(stored[2]?.mime).toBe("application/json");
1652
- expect(stored[2]?.correlationid).toBe("run-2");
1653
- });
1654
-
1655
- it("stores the page api map per origin", async () => {
1656
- const memory = new sessionmemory(new fakeadapter());
1657
- const entry = {
1658
- endpoint: "https://api.example/items",
1659
- method: "GET",
1660
- mime: "application/json",
1661
- frequency: 3,
1662
- payloadshape: ["id"],
1663
- jsonshare: 1,
1664
- stability: 1,
1665
- origin: "https://api.example",
1666
- correlationids: ["run-1"],
1667
- };
1668
- await memory.setapimap("https://api.example", [entry]);
1669
- await memory.setapimap("https://api.example", [{ ...entry, frequency: 7 }]);
1670
- await memory.setapimap("https://other.example", [
1671
- { ...entry, origin: "https://other.example", endpoint: "https://other.example/x" },
1672
- ]);
1673
- const stored = await memory.getapimap();
1674
- expect(stored).toHaveLength(2);
1675
- expect(stored.find((item) => item.origin === "https://api.example")?.frequency).toBe(7);
1676
- });
1677
- });
1678
-
1679
- describe("network control storage", () => {
1680
- it("stores block, mock and rewrite rule sets per run with hit counters and reverts", async () => {
1681
- const store = new sessionmemory(new fakeadapter());
1682
- await store.addblockrule({
1683
- id: "b1",
1684
- runid: "run1",
1685
- stepid: "s1",
1686
- urlpattern: "https://ads.example/*",
1687
- hits: 2,
1688
- registeredat: 1,
1689
- });
1690
- await store.addblockrule({
1691
- id: "b1",
1692
- runid: "run1",
1693
- stepid: "s1",
1694
- urlpattern: "https://ads.example/*",
1695
- hits: 3,
1696
- revertedat: 9,
1697
- registeredat: 1,
1698
- });
1699
- expect((await store.getblockrules())[0]).toMatchObject({ id: "b1", hits: 3, revertedat: 9 });
1700
- await store.addmockspec({
1701
- id: "m1",
1702
- runid: "run1",
1703
- stepid: "s1",
1704
- urlpattern: "https://api.example/status",
1705
- status: 204,
1706
- body: "fixture",
1707
- reviewed: true,
1708
- hits: 1,
1709
- registeredat: 1,
1710
- });
1711
- expect((await store.getmockspecs())[0]?.body).toBe("fixture");
1712
- await store.addheaderule({
1713
- id: "h1",
1714
- runid: "run1",
1715
- stepid: "s1",
1716
- urlpattern: "https://api.example/*",
1717
- name: "accept",
1718
- operation: "set",
1719
- value: "json",
1720
- hits: 4,
1721
- registeredat: 1,
1722
- });
1723
- expect((await store.getheaderules())[0]).toMatchObject({ operation: "set", hits: 4 });
1724
- });
1725
-
1726
- it("stores cookie operations per domain with timestamps and no values", async () => {
1727
- const store = new sessionmemory(new fakeadapter());
1728
- await store.addcookieop({
1729
- id: "c1",
1730
- runid: "run1",
1731
- stepid: "s1",
1732
- kind: "write",
1733
- domain: "example.com",
1734
- names: ["session"],
1735
- at: 1,
1736
- });
1737
- await store.addcookieop({
1738
- id: "c2",
1739
- runid: "run1",
1740
- stepid: "s2",
1741
- kind: "clear",
1742
- domain: "api.example.com",
1743
- names: [],
1744
- at: 2,
1745
- });
1746
- expect((await store.getcookieops()).map((op) => op.kind)).toEqual(["clear", "write"]);
1747
- expect((await store.getcookieops("example.com")).map((op) => op.id)).toEqual(["c1"]);
1748
- expect(JSON.stringify(await store.getcookieops())).not.toContain("value");
1749
- });
1750
-
1751
- it("stores token metadata per provider without token values and lists per provider", async () => {
1752
- const store = new sessionmemory(new fakeadapter());
1753
- await store.addtoken({
1754
- id: "t1",
1755
- provider: "providerco",
1756
- origin: "https://api.example",
1757
- scopes: ["read"],
1758
- accessstorageid: "token-t1-access",
1759
- refreshstorageid: "token-t1-refresh",
1760
- expiresat: 99,
1761
- at: 1,
1762
- });
1763
- await store.addtoken({
1764
- id: "t2",
1765
- provider: "otherco",
1766
- origin: "https://other.example",
1767
- scopes: ["write"],
1768
- accessstorageid: "token-t2-access",
1769
- expiresat: 99,
1770
- at: 2,
1771
- });
1772
- expect((await store.listtokens()).map((token) => token.provider)).toEqual(["otherco", "providerco"]);
1773
- expect((await store.listtokens("providerco")).map((token) => token.id)).toEqual(["t1"]);
1774
- await store.addtoken({
1775
- id: "t1",
1776
- provider: "providerco",
1777
- origin: "https://api.example",
1778
- scopes: ["read"],
1779
- accessstorageid: "token-t1-access",
1780
- refreshstorageid: "token-t1-refresh",
1781
- expiresat: 99,
1782
- refreshedat: 5,
1783
- at: 1,
1784
- });
1785
- expect((await store.listtokens("providerco"))[0]?.refreshedat).toBe(5);
1786
- });
1787
-
1788
- it("stores api key entries with last use timestamps and proxy route history with apply and revert times", async () => {
1789
- const store = new sessionmemory(new fakeadapter());
1790
- await store.setapikey({
1791
- name: "k",
1792
- origins: ["https://api.example"],
1793
- header: "authorization",
1794
- storageid: "apikey-k",
1795
- createdat: 1,
1796
- });
1797
- expect((await store.getapikeys())[0]?.lastuse).toBeUndefined();
1798
- await store.touchapikey("k", 42);
1799
- expect((await store.getapikeys())[0]?.lastuse).toBe(42);
1800
- await store.addproxyroute({
1801
- id: "p1",
1802
- runid: "run1",
1803
- stepid: "s1",
1804
- scheme: "socks5",
1805
- host: "proxy.example",
1806
- port: 1080,
1807
- bypass: ["https://api.example"],
1808
- appliedat: 1,
1809
- });
1810
- await store.addproxyroute({
1811
- id: "p1",
1812
- runid: "run1",
1813
- stepid: "s1",
1814
- scheme: "socks5",
1815
- host: "proxy.example",
1816
- port: 1080,
1817
- bypass: ["https://api.example"],
1818
- appliedat: 1,
1819
- revertedat: 7,
1820
- });
1821
- expect((await store.getproxyroutes())[0]).toMatchObject({ appliedat: 1, revertedat: 7 });
1822
- });
1823
-
1824
- it("expires rate limit states at their reset windows", async () => {
1825
- const store = new sessionmemory(new fakeadapter());
1826
- await store.setratelimit({ origin: "https://api.example", remaining: 3, limit: 10, resetat: 100, at: 1 });
1827
- await store.setratelimit({ origin: "https://slow.example", remaining: 0, resetat: 1000, at: 1 });
1828
- expect((await store.getratelimits(150)).map((read) => read.origin)).toEqual(["https://slow.example"]);
1829
- expect(await store.getratelimits(2000)).toEqual([]);
1830
- });
1831
- });
1832
-
1833
- describe("run timeline storage", () => {
1834
- it("stores timeline entries filtered by run, level and step id", async () => {
1835
- const store = new sessionmemory(new fakeadapter());
1836
- await store.addtimelineentry({
1837
- id: "t1",
1838
- runid: "run1",
1839
- stepid: "s1",
1840
- time: 3,
1841
- level: "error",
1842
- source: "error",
1843
- message: "boom",
1844
- });
1845
- await store.addtimelineentry({
1846
- id: "t2",
1847
- runid: "run1",
1848
- stepid: "s2",
1849
- time: 4,
1850
- level: "info",
1851
- source: "console",
1852
- message: "hello",
1853
- });
1854
- await store.addtimelineentry({
1855
- id: "t3",
1856
- runid: "run2",
1857
- stepid: "s1",
1858
- time: 5,
1859
- level: "error",
1860
- source: "network",
1861
- message: "failed request",
1862
- });
1863
- expect((await store.gettimeline()).map((entry) => entry.id)).toEqual(["t3", "t2", "t1"]);
1864
- expect((await store.listtimeline({ runid: "run1" })).map((entry) => entry.id)).toEqual(["t2", "t1"]);
1865
- expect((await store.listtimeline({ level: "error" })).map((entry) => entry.id)).toEqual(["t3", "t1"]);
1866
- expect((await store.listtimeline({ runid: "run1", stepid: "s2" })).map((entry) => entry.id)).toEqual(["t2"]);
1867
- });
1868
-
1869
- it("expires timeline entries into level count summaries under the retention window", async () => {
1870
- const store = new sessionmemory(new fakeadapter());
1871
- await store.setsettings({ timelineretention: 2 });
1872
- await store.addtimelineentry({
1873
- id: "t1",
1874
- runid: "run1",
1875
- stepid: "s1",
1876
- time: 1,
1877
- level: "error",
1878
- source: "error",
1879
- message: "one",
1880
- });
1881
- await store.addtimelineentry({
1882
- id: "t2",
1883
- runid: "run1",
1884
- stepid: "s1",
1885
- time: 2,
1886
- level: "warn",
1887
- source: "console",
1888
- message: "two",
1889
- });
1890
- await store.addtimelineentry({
1891
- id: "t3",
1892
- runid: "run1",
1893
- stepid: "s1",
1894
- time: 3,
1895
- level: "error",
1896
- source: "error",
1897
- message: "three",
1898
- });
1899
- expect((await store.gettimeline()).map((entry) => entry.id)).toEqual(["t3", "t2"]);
1900
- const summaries = await store.getlevelsummaries();
1901
- expect(summaries).toHaveLength(1);
1902
- expect(summaries[0]).toMatchObject({ runid: "run1", counts: { error: 1 } });
1903
- });
1904
-
1905
- it("stores error and rejection records with stack frames and long tasks with attributions", async () => {
1906
- const store = new sessionmemory(new fakeadapter());
1907
- await store.adderrorrecord({
1908
- id: "e1",
1909
- runid: "run1",
1910
- stepid: "s1",
1911
- message: "boom",
1912
- frames: [{ functionname: "load", url: "https://example.com/app.js", line: 42, column: 7 }],
1913
- sourceurl: "https://example.com/app.js",
1914
- line: 42,
1915
- at: 1,
1916
- });
1917
- await store.addrejectionrecord({
1918
- id: "r1",
1919
- runid: "run1",
1920
- stepid: "s1",
1921
- reason: "TypeError: failed",
1922
- frames: [],
1923
- at: 2,
1924
- });
1925
- await store.addlongtask({
1926
- id: "l1",
1927
- runid: "run1",
1928
- stepid: "s1",
1929
- duration: 90,
1930
- starttime: 10,
1931
- attributions: ["same-origin"],
1932
- at: 3,
1933
- });
1934
- expect((await store.geterrorrecords())[0]?.frames[0]).toMatchObject({ functionname: "load", line: 42 });
1935
- expect((await store.getrejectionrecords())[0]?.reason).toBe("TypeError: failed");
1936
- expect((await store.getlongtasks())[0]).toMatchObject({ duration: 90, attributions: ["same-origin"] });
1937
- });
1938
-
1939
- it("stores the console diff result, rotation targets and per origin console consents", async () => {
1940
- const store = new sessionmemory(new fakeadapter());
1941
- await store.addconsolediff({
1942
- base: "run1",
1943
- target: "run2",
1944
- lines: [{ kind: "added", text: "fresh" }],
1945
- added: 1,
1946
- removed: 0,
1947
- repeated: 0,
1948
- at: 9,
1949
- });
1950
- expect((await store.getdiff())?.base).toBe("run1");
1951
- await store.addrotationtarget({ target: "overflow1", runid: "run1", entries: 12, at: 9 });
1952
- await store.addrotationtarget({ target: "overflow1", runid: "run2", entries: 3, at: 10 });
1953
- expect((await store.getrotationtargets()).map((record) => record.runid)).toEqual(["run2", "run1"]);
1954
- await store.setconsoleconsent({
1955
- id: "c1",
1956
- prompt: "Console capture on https://example.com",
1957
- origin: "https://example.com",
1958
- stepid: "s1",
1959
- approved: true,
1960
- at: 1,
1961
- });
1962
- expect((await store.getconsoleconsents())[0]).toMatchObject({ origin: "https://example.com", approved: true });
1963
- });
1964
- });
1965
-
1966
- describe("devtools session storage", () => {
1967
- it("stores session records with domains and command outcomes with durations and error classes", async () => {
1968
- const store = new sessionmemory(new fakeadapter());
1969
- await store.setcdpsession({
1970
- id: "cdp1",
1971
- runid: "plan",
1972
- stepid: "a1",
1973
- tabid: 4,
1974
- origin: "https://example.com",
1975
- attachedat: 1,
1976
- domains: ["Runtime", "Debugger"],
1977
- debuggerversion: "devthink instrumented harness 1.1.46 (no chrome.debugger permission)",
1978
- });
1979
- await store.setcdpsession({
1980
- id: "cdp2",
1981
- runid: "plan",
1982
- stepid: "a2",
1983
- tabid: 4,
1984
- origin: "https://example.com",
1985
- attachedat: 2,
1986
- domains: ["Log"],
1987
- debuggerversion: "harness",
1988
- detachedat: 30,
1989
- });
1990
- expect((await store.getcdpsessions()).map((session) => session.id)).toEqual(["cdp2", "cdp1"]);
1991
- expect((await store.getcdpsessions())[0]?.domains).toEqual(["Log"]);
1992
- await store.addcdpcommand({
1993
- id: "cmd1",
1994
- sessionid: "cdp1",
1995
- runid: "plan",
1996
- stepid: "c1",
1997
- method: "Runtime.evaluate",
1998
- domain: "Runtime",
1999
- duration: 12,
2000
- at: 3,
2001
- });
2002
- await store.addcdpcommand({
2003
- id: "cmd2",
2004
- sessionid: "cdp1",
2005
- runid: "plan",
2006
- stepid: "c2",
2007
- method: "DOM.getSnapshot",
2008
- domain: "DOM",
2009
- duration: 4,
2010
- errorclass: "uninstrumented",
2011
- at: 4,
2012
- });
2013
- const commands = await store.getcdpcommands();
2014
- expect(commands[0]).toMatchObject({ method: "DOM.getSnapshot", errorclass: "uninstrumented" });
2015
- expect(commands[1]).toMatchObject({ method: "Runtime.evaluate", duration: 12 });
2016
- });
2017
-
2018
- it("stores event rules, breakpoints with hit counts, watch expressions and overrides with provenance", async () => {
2019
- const store = new sessionmemory(new fakeadapter());
2020
- await store.setcdpeventrule({
2021
- id: "r1",
2022
- sessionid: "cdp1",
2023
- runid: "plan",
2024
- stepid: "w1",
2025
- domain: "Log",
2026
- event: "entryAdded",
2027
- match: "fixture",
2028
- events: 4,
2029
- registeredat: 1,
2030
- });
2031
- await store.setcdpeventrule({
2032
- id: "r1",
2033
- sessionid: "cdp1",
2034
- runid: "plan",
2035
- stepid: "w1",
2036
- domain: "Log",
2037
- event: "entryAdded",
2038
- match: "fixture",
2039
- events: 6,
2040
- registeredat: 1,
2041
- closedat: 9,
2042
- });
2043
- expect(await store.getcdpeventrules()).toHaveLength(1);
2044
- expect((await store.getcdpeventrules())[0]).toMatchObject({ events: 6, closedat: 9 });
2045
- await store.addbreakpoint({
2046
- id: "bp1",
2047
- runid: "plan",
2048
- stepid: "b1",
2049
- url: "https://example.com/app.js",
2050
- line: 5,
2051
- condition: "items.length > 0",
2052
- hits: 1,
2053
- registeredat: 1,
2054
- });
2055
- await store.addbreakpoint({
2056
- id: "bp1",
2057
- runid: "plan",
2058
- stepid: "b1",
2059
- url: "https://example.com/app.js",
2060
- line: 5,
2061
- condition: "items.length > 0",
2062
- hits: 4,
2063
- registeredat: 1,
2064
- revertedat: 8,
2065
- });
2066
- expect(await store.getbreakpoints()).toHaveLength(1);
2067
- expect((await store.getbreakpoints())[0]).toMatchObject({ hits: 4, revertedat: 8 });
2068
- await store.setwatchexpression({
2069
- id: "we1",
2070
- runid: "plan",
2071
- stepid: "w1",
2072
- expression: "items.length",
2073
- scope: "topframe",
2074
- reviewed: true,
2075
- values: [{ pauseid: "p1", value: "3", at: 4 }],
2076
- at: 2,
2077
- });
2078
- await store.setwatchexpression({
2079
- id: "we1",
2080
- runid: "plan",
2081
- stepid: "w1",
2082
- expression: "items.length",
2083
- scope: "topframe",
2084
- reviewed: true,
2085
- values: [
2086
- { pauseid: "p1", value: "3", at: 4 },
2087
- { pauseid: "p2", value: "8", at: 6 },
2088
- ],
2089
- at: 2,
2090
- });
2091
- expect((await store.getwatchexpressions())[0]?.values).toHaveLength(2);
2092
- await store.addscriptoverride({
2093
- id: "ov1",
2094
- runid: "plan",
2095
- stepid: "o1",
2096
- urlpattern: "https://cdn.example/vendor.js",
2097
- source: "window.fixture = true;",
2098
- reviewed: true,
2099
- reviewedat: 3,
2100
- hits: 2,
2101
- appliedat: 3,
2102
- });
2103
- expect((await store.getscriptoverrides())[0]).toMatchObject({ reviewed: true, hits: 2 });
2104
- });
2105
-
2106
- it("stores pause states per run and expires the call frames under the retention window", async () => {
2107
- const store = new sessionmemory(new fakeadapter());
2108
- await store.addpause({
2109
- id: "p1",
2110
- runid: "plan",
2111
- stepid: "b1",
2112
- reason: "breakpoint",
2113
- callframes: [{ url: "https://example.com/app.js", line: 5 }],
2114
- hitbreakpoint: "bp1",
2115
- domsnapshotid: "dom-1",
2116
- at: 1,
2117
- });
2118
- await store.addpause({
2119
- id: "p2",
2120
- runid: "plan",
2121
- stepid: "b1",
2122
- reason: "step",
2123
- callframes: [{ url: "https://example.com/app.js", line: 6 }],
2124
- at: 2,
2125
- });
2126
- await store.addpause({ id: "p3", runid: "other", stepid: "b1", reason: "breakpoint", callframes: [], at: 3 });
2127
- expect((await store.listpauses("plan")).map((pause) => pause.id)).toEqual(["p2", "p1"]);
2128
- expect((await store.listpauses("other")).map((pause) => pause.id)).toEqual(["p3"]);
2129
- const retained = new sessionmemory(new fakeadapter());
2130
- await retained.setsettings({ pauseretention: 2 });
2131
- await retained.addpause({
2132
- id: "p1",
2133
- runid: "plan",
2134
- stepid: "b1",
2135
- reason: "breakpoint",
2136
- callframes: [{ url: "https://example.com/app.js", line: 5 }],
2137
- hitbreakpoint: "bp1",
2138
- domsnapshotid: "dom-1",
2139
- at: 1,
2140
- });
2141
- await retained.addpause({
2142
- id: "p2",
2143
- runid: "plan",
2144
- stepid: "b1",
2145
- reason: "step",
2146
- callframes: [{ url: "https://example.com/app.js", line: 6 }],
2147
- at: 2,
2148
- });
2149
- await retained.addpause({
2150
- id: "p3",
2151
- runid: "plan",
2152
- stepid: "b1",
2153
- reason: "step",
2154
- callframes: [{ url: "https://example.com/app.js", line: 7 }],
2155
- at: 3,
2156
- });
2157
- const pauses = await retained.getpauses();
2158
- expect(pauses).toHaveLength(3);
2159
- const expired = pauses.find((pause) => pause.id === "p1");
2160
- if (!expired) throw new Error("The expired pause must stay stored.");
2161
- expect(expired.framesexpired).toBe(true);
2162
- expect(expired.callframes).toEqual([]);
2163
- expect(expired.domsnapshotid).toBeUndefined();
2164
- expect(expired.reason).toBe("breakpoint");
2165
- expect(expired.hitbreakpoint).toBe("bp1");
2166
- const kept = pauses.find((pause) => pause.id === "p3");
2167
- expect(kept?.framesexpired).toBeUndefined();
2168
- expect(kept?.callframes).toHaveLength(1);
2169
- });
2170
-
2171
- it("stores debugger consents per origin and revokes every approved record of one origin", async () => {
2172
- const store = new sessionmemory(new fakeadapter());
2173
- await store.setdebuggergrant({
2174
- id: "g1",
2175
- prompt: "Debugger attach on https://example.com",
2176
- origin: "https://example.com",
2177
- domains: ["Runtime"],
2178
- approved: true,
2179
- consentedat: 1,
2180
- });
2181
- await store.setdebuggergrant({
2182
- id: "g2",
2183
- prompt: "Debugger attach on https://api.example",
2184
- origin: "https://api.example",
2185
- domains: ["Network"],
2186
- approved: true,
2187
- consentedat: 2,
2188
- });
2189
- expect(await store.getdebuggergrants()).toHaveLength(2);
2190
- const revoked = await store.revokedebuggergrants("https://example.com", 9);
2191
- expect(revoked).toBe(1);
2192
- const grants = await store.getdebuggergrants();
2193
- expect(grants.find((grant) => grant.id === "g1")?.revokedat).toBe(9);
2194
- expect(grants.find((grant) => grant.id === "g2")?.revokedat).toBeUndefined();
2195
- expect(await store.revokedebuggergrants("https://example.com", 10)).toBe(0);
2196
- });
2197
- });
2198
-
2199
- describe("profiler storage", () => {
2200
- const now = 1_800_000_000_000;
2201
-
2202
- it("stores flow metric series per run and heap records with byte and node counts", async () => {
2203
- const store = new sessionmemory(new fakeadapter());
2204
- await store.addflowmetric({
2205
- id: "f1",
2206
- runid: "plan",
2207
- stepid: "m1",
2208
- name: "navigation",
2209
- start: 0,
2210
- end: 800,
2211
- duration: 800,
2212
- steps: ["s1"],
2213
- at: now,
2214
- });
2215
- await store.addflowmetric({
2216
- id: "f2",
2217
- runid: "plan",
2218
- stepid: "m1",
2219
- name: "blocking",
2220
- start: 0,
2221
- end: 100,
2222
- duration: 70,
2223
- steps: ["s1"],
2224
- at: now,
2225
- });
2226
- await store.addflowmetric({
2227
- id: "f3",
2228
- runid: "other",
2229
- stepid: "m1",
2230
- name: "navigation",
2231
- start: 0,
2232
- end: 10,
2233
- duration: 10,
2234
- steps: [],
2235
- at: now,
2236
- });
2237
- expect((await store.listflowmetrics("plan")).map((metric) => metric.name)).toEqual(["blocking", "navigation"]);
2238
- expect(await store.getflowmetrics()).toHaveLength(3);
2239
- await store.setheaprecord({
2240
- id: "h1",
2241
- runid: "plan",
2242
- stepid: "hs",
2243
- origin: "https://example.com",
2244
- bytesize: 12_000_000,
2245
- nodecount: 1450,
2246
- capturedat: now,
2247
- });
2248
- expect((await store.getheaprecords())[0]).toMatchObject({ bytesize: 12_000_000, nodecount: 1450 });
2249
- expect((await store.getheaprecords())[0]?.bytesexpired).toBeUndefined();
2250
- });
2251
-
2252
- it("stores growth samples with computed trends and cpu profiles with hot functions", async () => {
2253
- const store = new sessionmemory(new fakeadapter());
2254
- await store.addgrowsample({
2255
- id: "g1",
2256
- runid: "plan",
2257
- stepid: "s1",
2258
- usedbytes: 1_000_000,
2259
- limitbytes: 4_000_000,
2260
- at: now,
2261
- });
2262
- await store.addgrowsample({
2263
- id: "g2",
2264
- runid: "plan",
2265
- stepid: "s2",
2266
- usedbytes: 3_000_000,
2267
- limitbytes: 4_000_000,
2268
- at: now + 1000,
2269
- });
2270
- expect(await store.listgrowsamples("plan")).toHaveLength(2);
2271
- await store.settrend({ runid: "plan", slope: 2000, samples: 2, flaggedsteps: ["s2"], at: now + 2000 });
2272
- await store.settrend({ runid: "plan", slope: 2000, samples: 2, flaggedsteps: ["s2", "s3"], at: now + 3000 });
2273
- expect(await store.gettrends()).toHaveLength(1);
2274
- expect((await store.gettrends())[0]?.flaggedsteps).toEqual(["s2", "s3"]);
2275
- await store.setcpuprofile({
2276
- id: "c1",
2277
- runid: "plan",
2278
- stepid: "cpu",
2279
- origin: "https://example.com",
2280
- duration: 500,
2281
- samplecount: 3,
2282
- hotfunctions: ["render", "parse"],
2283
- at: now,
2284
- });
2285
- expect((await store.getcpuprofiles())[0]).toMatchObject({
2286
- duration: 500,
2287
- samplecount: 3,
2288
- hotfunctions: ["render", "parse"],
2289
- });
2290
- });
2291
-
2292
- it("stores shift entries, traces with annotations and source map references with per origin consents", async () => {
2293
- const store = new sessionmemory(new fakeadapter());
2294
- await store.addshiftentry({
2295
- id: "sh1",
2296
- runid: "plan",
2297
- stepid: "ws",
2298
- score: 0.12,
2299
- starttime: 40,
2300
- selectors: ["img#hero"],
2301
- at: now,
2302
- });
2303
- expect((await store.getshiftentries())[0]).toMatchObject({ score: 0.12, selectors: ["img#hero"] });
2304
- await store.settracerecord({
2305
- id: "t1",
2306
- runid: "plan",
2307
- stepid: "tl",
2308
- origin: "https://example.com",
2309
- categories: ["scripting", "network"],
2310
- bytesize: 400,
2311
- events: 2,
2312
- annotations: [{ stepid: "s1", label: "open", offset: 100 }],
2313
- startedat: now,
2314
- endedat: now + 500,
2315
- });
2316
- await store.settracerecord({
2317
- id: "t2",
2318
- runid: "other",
2319
- stepid: "tl",
2320
- origin: "https://example.com",
2321
- categories: ["scripting"],
2322
- bytesize: 40,
2323
- events: 1,
2324
- annotations: [],
2325
- startedat: now,
2326
- endedat: now + 500,
2327
- });
2328
- expect((await store.listtraces({ runid: "plan" })).map((trace) => trace.id)).toEqual(["t1"]);
2329
- expect((await store.listtraces({ categories: ["scripting"] })).map((trace) => trace.id)).toEqual(["t2", "t1"]);
2330
- expect(
2331
- (await store.listtraces({ runid: "plan", categories: ["scripting", "network"] })).map((trace) => trace.id),
2332
- ).toEqual(["t1"]);
2333
- await store.settracefile("t1", '{"devthinktrace":"1.1.47"}');
2334
- expect(await store.gettracefile("t1")).toBe('{"devthinktrace":"1.1.47"}');
2335
- expect(await store.gettracefile("t2")).toBeUndefined();
2336
- await store.setsourcemapref({
2337
- id: "sm1",
2338
- runid: "plan",
2339
- stepid: "sm",
2340
- origin: "https://example.com",
2341
- scripturl: "https://example.com/app.js",
2342
- mapurl: "https://example.com/app.js.map",
2343
- parsed: true,
2344
- at: now,
2345
- });
2346
- expect((await store.getsourcemaps())[0]).toMatchObject({ parsed: true });
2347
- await store.setsourcemapconsent({
2348
- id: "c1",
2349
- prompt: "Source map capture prompt",
2350
- origin: "https://example.com",
2351
- consentedat: now,
2352
- });
2353
- await store.setsourcemapconsent({
2354
- id: "c1",
2355
- prompt: "Source map capture prompt",
2356
- origin: "https://example.com",
2357
- approved: true,
2358
- consentedat: now,
2359
- usedat: now + 1,
2360
- });
2361
- expect((await store.getsourcemapconsents())[0]?.approved).toBe(true);
2362
- expect(await store.revokesourcemapconsents("https://example.com", now + 2)).toBe(1);
2363
- expect((await store.getsourcemapconsents())[0]?.revokedat).toBe(now + 2);
2364
- });
2365
-
2366
- it("expires the heavy profile bytes after the configured retention window while the metadata survives", async () => {
2367
- const store = new sessionmemory(new fakeadapter());
2368
- await store.setsettings({ profileretention: 1000 });
2369
- const stale = Date.now() - 2000;
2370
- await store.setheaprecord({
2371
- id: "h1",
2372
- runid: "plan",
2373
- stepid: "hs",
2374
- origin: "https://example.com",
2375
- bytesize: 12_000_000,
2376
- nodecount: 1450,
2377
- capturedat: stale,
2378
- });
2379
- expect((await store.getheaprecords())[0]?.bytesexpired).toBe(true);
2380
- expect((await store.getheaprecords())[0]).toMatchObject({ bytesize: 12_000_000, nodecount: 1450 });
2381
- await store.setcpuprofile({
2382
- id: "c1",
2383
- runid: "plan",
2384
- stepid: "cpu",
2385
- origin: "https://example.com",
2386
- duration: 500,
2387
- samplecount: 3,
2388
- hotfunctions: ["render"],
2389
- at: stale,
2390
- });
2391
- expect((await store.getcpuprofiles())[0]?.samplesexpired).toBe(true);
2392
- await store.settracerecord({
2393
- id: "t1",
2394
- runid: "plan",
2395
- stepid: "tl",
2396
- origin: "https://example.com",
2397
- categories: ["scripting"],
2398
- bytesize: 400,
2399
- events: 2,
2400
- annotations: [{ stepid: "s1", label: "open", offset: 100 }],
2401
- startedat: stale,
2402
- endedat: stale + 500,
2403
- });
2404
- const trace = (await store.gettracerecords())[0];
2405
- expect(trace?.bytesexpired).toBe(true);
2406
- expect(trace?.annotations).toHaveLength(1);
2407
- await store.settracefile("t1", '{"devthinktrace":"1.1.47"}');
2408
- expect(await store.gettracefile("t1")).toBeUndefined();
2409
- });
2410
- });
2411
-
2412
- describe("emulation memory", () => {
2413
- it("stores the emulation state per run with the layer history and expires reverted prior states after the retention window", async () => {
2414
- const store = new sessionmemory(new fakeadapter());
2415
- const layer = {
2416
- id: "l1",
2417
- runid: "run",
2418
- stepid: "s1",
2419
- family: "device" as const,
2420
- name: "phone",
2421
- originscope: "https://example.com",
2422
- appliedat: 1,
2423
- prior: { pixelratio: 2 },
2424
- revertplan: ["restore"],
2425
- };
2426
- await store.setemulationstate({
2427
- runid: "run",
2428
- tabid: 4,
2429
- origin: "https://example.com",
2430
- layers: [layer],
2431
- updatedat: 2,
2432
- });
2433
- expect((await store.getemulationstate("run"))?.layers[0]?.name).toBe("phone");
2434
- expect((await store.listlayers("run"))[0]?.revertplan).toEqual(["restore"]);
2435
- expect(await store.listlayers("other")).toEqual([]);
2436
- await store.setsettings({ emulationretention: 1 });
2437
- await store.setemulationstate({
2438
- runid: "run",
2439
- tabid: 4,
2440
- origin: "https://example.com",
2441
- layers: [{ ...layer, revertedat: Date.now() - 5 }],
2442
- updatedat: 3,
2443
- });
2444
- const expired = await store.listlayers("run");
2445
- expect(expired[0]?.prior).toBeUndefined();
2446
- expect(expired[0]?.priorexpired).toBe(true);
2447
- expect(expired[0]?.name).toBe("phone");
2448
- });
2449
-
2450
- it("stores the user curated device, network, location and agent preset libraries", async () => {
2451
- const store = new sessionmemory(new fakeadapter());
2452
- await store.setdevicepreset({ name: "phone", width: 390, height: 844, pixelratio: 3, mobile: true });
2453
- await store.setdevicepreset({ name: "tablet", width: 820, height: 1180, pixelratio: 2, mobile: true });
2454
- await store.setdevicepreset({ name: "phone", width: 391, height: 845, pixelratio: 3, mobile: true });
2455
- expect((await store.getdevicepresets()).map((preset) => preset.name).sort()).toEqual(["phone", "tablet"]);
2456
- expect((await store.getdevicepresets()).find((preset) => preset.name === "phone")?.width).toBe(391);
2457
- await store.setnetworkpreset({ name: "slow3g", latency: 400, download: 400, upload: 400, offline: true });
2458
- await store.setlocationpreset({ name: "lisbon", latitude: 38.7223, longitude: -9.1393, accuracy: 100 });
2459
- await store.setagentpreset({
2460
- name: "desktopmask",
2461
- useragent: "Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0.0.0 Safari/537.36",
2462
- platform: "Linux x86_64",
2463
- brands: ["Chromium"],
2464
- });
2465
- expect((await store.getnetworkpresets())[0]?.offline).toBe(true);
2466
- expect((await store.getlocationpresets())[0]?.latitude).toBe(38.7223);
2467
- expect((await store.getagentpresets())[0]?.brands).toEqual(["Chromium"]);
2468
- });
2469
-
2470
- it("stores blackbox rule sets per origin, permission override history and location consents", async () => {
2471
- const store = new sessionmemory(new fakeadapter());
2472
- await store.setblackboxrules("https://example.com", [
2473
- { urlpatterns: ["https://cdn.example/**"], tracescope: "both" },
2474
- ]);
2475
- await store.setblackboxrules("https://example.com", [
2476
- { urlpatterns: ["https://vendor.example/*"], tracescope: "traces" },
2477
- ]);
2478
- const rules = await store.getblackboxrules();
2479
- expect(rules).toHaveLength(1);
2480
- expect(rules[0]?.rules[0]?.urlpatterns).toEqual(["https://vendor.example/*"]);
2481
- await store.addpermissionoverride({
2482
- id: "p1",
2483
- runid: "run",
2484
- stepid: "s1",
2485
- origin: "https://example.com",
2486
- name: "geolocation",
2487
- state: "granted",
2488
- priorstate: "prompt",
2489
- appliedat: 1,
2490
- });
2491
- await store.addpermissionoverride({
2492
- id: "p1",
2493
- runid: "run",
2494
- stepid: "s1",
2495
- origin: "https://example.com",
2496
- name: "geolocation",
2497
- state: "granted",
2498
- priorstate: "prompt",
2499
- appliedat: 1,
2500
- restoredat: 2,
2501
- });
2502
- const overrides = await store.getpermissionoverrides();
2503
- expect(overrides).toHaveLength(1);
2504
- expect(overrides[0]?.restoredat).toBe(2);
2505
- await store.setlocationconsent({
2506
- id: "c1",
2507
- prompt: "Where?",
2508
- origin: "https://example.com",
2509
- latitude: 38.7223,
2510
- longitude: -9.1393,
2511
- consentedat: 1,
2512
- });
2513
- await store.setlocationconsent({
2514
- id: "c1",
2515
- prompt: "Where?",
2516
- origin: "https://example.com",
2517
- latitude: 38.7223,
2518
- longitude: -9.1393,
2519
- approved: true,
2520
- consentedat: 1,
2521
- });
2522
- const consents = await store.getlocationconsents();
2523
- expect(consents).toHaveLength(1);
2524
- expect(consents[0]?.approved).toBe(true);
2525
- });
2526
- });
2527
-
2528
- describe("workflow editor memory", () => {
2529
- it("stores workflow versions and version diffs for the timeline", async () => {
2530
- const store = new sessionmemory(new fakeadapter());
2531
- await store.addworkflowversion({
2532
- workflowid: "wf1",
2533
- version: 1,
2534
- createdat: 1,
2535
- note: "first",
2536
- steps: 2,
2537
- risk: "read",
2538
- });
2539
- await store.addworkflowversion({
2540
- workflowid: "wf1",
2541
- version: 2,
2542
- createdat: 2,
2543
- note: "second",
2544
- steps: 3,
2545
- risk: "read",
2546
- });
2547
- await store.addworkflowversion({
2548
- workflowid: "wf1",
2549
- version: 2,
2550
- createdat: 3,
2551
- note: "second again",
2552
- steps: 3,
2553
- risk: "read",
2554
- });
2555
- const versions = await store.listworkflowversions("wf1");
2556
- expect(versions.map((version) => version.note)).toEqual(["second again", "first"]);
2557
- expect(await store.listworkflowversions("wf2")).toEqual([]);
2558
- await store.addversiondiff({
2559
- workflowid: "wf1",
2560
- from: 1,
2561
- to: 2,
2562
- added: [{ stepid: "s3", kind: "wait", label: "Fresh" }],
2563
- removed: [],
2564
- changed: [],
2565
- at: 4,
2566
- });
2567
- await store.addversiondiff({
2568
- workflowid: "wf1",
2569
- from: 1,
2570
- to: 2,
2571
- added: [],
2572
- removed: [],
2573
- changed: [{ stepid: "s1", kind: "readtext", label: "Open", changes: ["label"] }],
2574
- at: 5,
2575
- });
2576
- const diffs = await store.listversiondiffs("wf1");
2577
- expect(diffs).toHaveLength(1);
2578
- expect(diffs[0]?.changed[0]?.changes).toEqual(["label"]);
2579
- });
2580
-
2581
- it("stores run history with retention and answers the filters", async () => {
2582
- const store = new sessionmemory(new fakeadapter());
2583
- for (const [index, outcome] of ["done", "failed", "done", "paused"].entries()) {
2584
- await store.addrunhistory({
2585
- runid: `r${index}`,
2586
- workflowid: index % 2 === 0 ? "wf1" : "wf2",
2587
- outcome,
2588
- steps: index,
2589
- total: 4,
2590
- duration: index,
2591
- cause: "manual",
2592
- startedat: 10 + index,
2593
- endedat: 20 + index,
2594
- });
2595
- }
2596
- expect((await store.gethistory()).map((entry) => entry.runid)).toEqual(["r3", "r2", "r1", "r0"]);
2597
- expect((await store.gethistory({ workflowid: "wf1" })).map((entry) => entry.runid)).toEqual(["r2", "r0"]);
2598
- expect((await store.gethistory({ outcome: "done", limit: 1 })).map((entry) => entry.runid)).toEqual(["r2"]);
2599
- expect((await store.gethistory({ since: 23 })).map((entry) => entry.runid)).toEqual(["r3"]);
2600
- await store.setsettings({ runhistoryretention: 2 });
2601
- await store.addrunhistory({
2602
- runid: "r4",
2603
- workflowid: "wf1",
2604
- outcome: "done",
2605
- steps: 4,
2606
- total: 4,
2607
- duration: 1,
2608
- cause: "cron",
2609
- startedat: 30,
2610
- endedat: 31,
2611
- dryrun: true,
2612
- });
2613
- const kept = await store.gethistory();
2614
- expect(kept.map((entry) => entry.runid)).toEqual(["r4", "r3"]);
2615
- expect(kept[0]?.dryrun).toBe(true);
2616
- expect(kept[0]?.cause).toBe("cron");
2617
- });
2618
-
2619
- it("stores editor layouts, breakpoints, overrides, watchdog events, imports and background flags", async () => {
2620
- const store = new sessionmemory(new fakeadapter());
2621
- await store.seteditorlayout("wf1", { width: 900, height: 700, viewportx: 12, viewporty: 34, zoom: 1.5 });
2622
- expect(await store.geteditorlayout("wf1")).toEqual({
2623
- width: 900,
2624
- height: 700,
2625
- viewportx: 12,
2626
- viewporty: 34,
2627
- zoom: 1.5,
2628
- });
2629
- expect(await store.geteditorlayout("wf2")).toBeUndefined();
2630
- await store.setworkflowbreakpoints("wf1", ["s1", "s3"]);
2631
- await store.setworkflowbreakpoints("wf1", ["s2"]);
2632
- expect(await store.getworkflowbreakpoints("wf1")).toEqual(["s2"]);
2633
- await store.addsiteoverride({
2634
- id: "o1",
2635
- workflowid: "wf1",
2636
- pattern: "https://example.com",
2637
- deltas: { waitms: 500 },
2638
- createdat: 1,
2639
- });
2640
- await store.addsiteoverride({
2641
- id: "o1",
2642
- workflowid: "wf1",
2643
- pattern: "https://example.com",
2644
- deltas: { delaybase: 250 },
2645
- createdat: 2,
2646
- });
2647
- await store.addsiteoverride({
2648
- id: "o2",
2649
- workflowid: "wf2",
2650
- pattern: "https://other.example",
2651
- deltas: {},
2652
- createdat: 3,
2653
- });
2654
- expect((await store.listsiteoverrides("wf1")).map((override) => override.deltas)).toEqual([{ delaybase: 250 }]);
2655
- await store.removesiteoverride("o2");
2656
- expect(await store.listsiteoverrides()).toHaveLength(1);
2657
- await store.addwatchdogevent({
2658
- id: "w1",
2659
- runid: "r1",
2660
- verdict: "stalled",
2661
- action: "pause",
2662
- outcome: "Paused.",
2663
- at: 1,
2664
- });
2665
- await store.addwatchdogevent({
2666
- id: "w2",
2667
- runid: "r2",
2668
- verdict: "zombie",
2669
- action: "reap",
2670
- outcome: "Reaped.",
2671
- at: 2,
2672
- });
2673
- expect((await store.listwatchdogevents()).map((event) => event.verdict)).toEqual(["zombie", "stalled"]);
2674
- const record = {
2675
- id: "wf9",
2676
- name: "imported",
2677
- version: 1,
2678
- origins: ["https://example.com"],
2679
- steps: [],
2680
- blocks: [],
2681
- risk: "read" as const,
2682
- createdat: 1,
2683
- };
2684
- await store.addworkflowimport({ id: "imp1", record, importedat: 5, filename: "flow.json" });
2685
- expect((await store.listworkflowimports()).map((entry) => entry.filename)).toEqual(["flow.json"]);
2686
- await store.removeworkflowimport("imp1");
2687
- expect(await store.listworkflowimports()).toEqual([]);
2688
- await store.setbackgroundruns({ wf1: true });
2689
- expect(await store.getbackgroundruns()).toEqual({ wf1: true });
2690
- await store.addworkflowrecord(record);
2691
- await store.removeworkflowversion("wf9", 1);
2692
- expect(await store.listworkflows()).toEqual([]);
2693
- });
2694
- });
2695
-
2696
- describe("agent protocol memory", () => {
2697
- it("stores client records with pairing state, negotiated capability sets and the server config and state", async () => {
2698
- const store = new sessionmemory(new fakeadapter());
2699
- const client = { id: "client1", transport: "stdio" as const, paired: false, connectedat: 10 };
2700
- await store.setclient(client);
2701
- await store.setclient({ ...client, id: "client2", transport: "http" as const, connectedat: 11 });
2702
- expect((await store.listclients()).map((entry) => entry.id)).toEqual(["client2", "client1"]);
2703
- await store.setclient({ ...client, paired: true, pairedat: 12 });
2704
- expect((await store.listclients()).find((entry) => entry.id === "client1")?.paired).toBe(true);
2705
- const capabilities = {
2706
- protocolversion: "1.1.54",
2707
- name: "devthink",
2708
- version: "1.1.54",
2709
- toolversion: 1,
2710
- tools: 30,
2711
- namespaces: ["browser" as const],
2712
- transports: ["stdio" as const],
2713
- };
2714
- await store.setclientcapabilities("client1", capabilities);
2715
- expect((await store.getclients()).find((entry) => entry.id === "client1")?.capabilities?.toolversion).toBe(1);
2716
- await store.setclient({
2717
- ...client,
2718
- id: "client2",
2719
- transport: "http" as const,
2720
- connectedat: 11,
2721
- disconnectedat: 13,
2722
- });
2723
- expect((await store.listclients()).map((entry) => entry.id)).toEqual(["client1"]);
2724
- await store.clearclients();
2725
- expect(await store.listclients()).toEqual([]);
2726
- await store.setmcpconfig({
2727
- port: 7436,
2728
- transports: ["stdio"],
2729
- enabled: true,
2730
- framesize: 50_000,
2731
- queuedepth: 20,
2732
- callretention: 10,
2733
- });
2734
- expect((await store.getmcpconfig())?.framesize).toBe(50_000);
2735
- await store.setmcpstate({
2736
- state: "running",
2737
- startedat: 20,
2738
- bridge: {
2739
- id: "bridge1",
2740
- host: "com.wenathlan.devthink",
2741
- connected: true,
2742
- pid: 4242,
2743
- startedat: 20,
2744
- restarts: 0,
2745
- received: 0,
2746
- sent: 0,
2747
- },
2748
- });
2749
- expect((await store.getmcpstate())?.bridge?.pid).toBe(4242);
2750
- expect(await store.getmcpstate()).toMatchObject({ state: "running" });
2751
- });
2752
-
2753
- it("stores bridge launch events with process ids and tool call records under the user configured retention", async () => {
2754
- const store = new sessionmemory(new fakeadapter());
2755
- await store.addbridgelaunch({ id: "l1", host: "com.wenathlan.devthink", pid: 4242, restart: false, at: 30 });
2756
- await store.addbridgelaunch({ id: "l2", host: "com.wenathlan.devthink", pid: 5252, restart: true, at: 31 });
2757
- expect((await store.listbridgelaunches()).map((launch) => launch.pid)).toEqual([5252, 4242]);
2758
- await store.addtoolcall({
2759
- id: "t1",
2760
- clientid: "client1",
2761
- tool: "browser.click",
2762
- origin: "https://example.com",
2763
- ok: true,
2764
- at: 40,
2765
- });
2766
- await store.addtoolcall({
2767
- id: "t2",
2768
- clientid: "client1",
2769
- tool: "browser.readtext",
2770
- origin: "https://example.com",
2771
- ok: false,
2772
- code: "consentrefused",
2773
- at: 41,
2774
- });
2775
- expect((await store.listtoolcalls()).map((call) => call.id)).toEqual(["t2", "t1"]);
2776
- await store.setmcpconfig({ port: 7436, transports: ["stdio"], enabled: true, callretention: 1 });
2777
- await store.addtoolcall({
2778
- id: "t3",
2779
- clientid: "client2",
2780
- tool: "browser.snapshot",
2781
- origin: "https://example.com",
2782
- ok: true,
2783
- at: 42,
2784
- });
2785
- expect((await store.listtoolcalls()).map((call) => call.id)).toEqual(["t3"]);
2786
- });
2787
-
2788
- it("records tool list, negotiate, connect and tool call audit events", async () => {
2789
- const store = new sessionmemory(new fakeadapter());
2790
- await store.addaudi({
2791
- id: "a1",
2792
- kind: "protocol",
2793
- at: 50,
2794
- summary: "A new mcp client client1 connected on the stdio transport and waits for the pairing approval.",
2795
- });
2796
- await store.addaudi({
2797
- id: "a2",
2798
- kind: "protocol",
2799
- at: 51,
2800
- summary: "The mcp client client1 listed the tool catalog of 30 tools.",
2801
- });
2802
- await store.addaudi({
2803
- id: "a3",
2804
- kind: "protocol",
2805
- at: 52,
2806
- summary: "The mcp client client1 negotiated its capability set with the server.",
2807
- });
2808
- await store.addaudi({
2809
- id: "a4",
2810
- kind: "tool",
2811
- at: 53,
2812
- summary:
2813
- "The mcp client client1 called the browser.click tool on https://example.com and it ran behind the consent gates.",
2814
- sessionid: "sess",
2815
- });
2816
- const audit = await store.getaudit();
2817
- expect(audit.map((event) => event.kind)).toEqual(["tool", "protocol", "protocol", "protocol"]);
2818
- expect(audit[0]?.summary).toContain("browser.click");
2819
- expect(audit[3]?.summary).toContain("pairing approval");
2820
- });
2821
- });
2822
-
2823
- describe("sessionmemory agent protocol part two", () => {
2824
- const now = 1_800_000_000_000;
2825
-
2826
- it("stores session tokens only in their tokenhash form", async () => {
2827
- const store = new sessionmemory(new fakeadapter());
2828
- await store.setsessiontokens([
2829
- { id: "t1", clientid: "client1", hash: "sha256:abc", scopes: ["browser"], issuedat: now, expiresat: now + 1000 },
2830
- ]);
2831
- const tokens = await store.getsessiontokens();
2832
- expect(tokens[0]?.hash).toBe("sha256:abc");
2833
- expect(JSON.stringify(tokens)).not.toContain("rawtoken");
2834
- await store.setsessiontokens([
2835
- ...tokens,
2836
- { id: "t2", clientid: "client2", hash: "sha256:def", scopes: ["memory"], issuedat: now, expiresat: now + 1000 },
2837
- ]);
2838
- expect((await store.getsessiontokens()).map((token) => token.id)).toEqual(["t1", "t2"]);
2839
- });
2840
-
2841
- it("stores pairing codes with their single use state", async () => {
2842
- const store = new sessionmemory(new fakeadapter());
2843
- await store.addpairingcode({ code: "DT-ONE", scopes: ["browser"], issuedat: now, expiresat: now + 1000 });
2844
- await store.addpairingcode({ code: "DT-TWO", scopes: ["memory"], issuedat: now, expiresat: now + 1000 });
2845
- expect((await store.getpairingcodes()).map((code) => code.code)).toEqual(["DT-TWO", "DT-ONE"]);
2846
- await store.addpairingcode({ code: "DT-TWO", scopes: ["system"], issuedat: now, expiresat: now + 1000 });
2847
- expect((await store.getpairingcodes()).filter((code) => code.code === "DT-TWO")).toHaveLength(1);
2848
- await store.usepairingcode("DT-ONE", now + 500);
2849
- expect((await store.getpairingcodes()).find((code) => code.code === "DT-ONE")?.usedat).toBe(now + 500);
2850
- await store.usepairingcode("DT-MISSING", now);
2851
- expect((await store.getpairingcodes()).find((code) => code.code === "DT-ONE")?.usedat).toBe(now + 500);
2852
- });
2853
-
2854
- it("stores allowlist entries, client identities and stream channels", async () => {
2855
- const store = new sessionmemory(new fakeadapter());
2856
- await store.setclientidentity({ fingerprint: "aa11", displayname: "Laptop agent" });
2857
- await store.setallowlistentry({
2858
- fingerprint: "aa11",
2859
- displayname: "Laptop agent",
2860
- namespaces: ["browser"],
2861
- grantedat: now,
2862
- history: [{ at: now, actor: "user", change: "Granted." }],
2863
- });
2864
- await store.setallowlistentry({
2865
- fingerprint: "bb22",
2866
- displayname: "Desk agent",
2867
- namespaces: ["memory"],
2868
- grantedat: now,
2869
- history: [],
2870
- });
2871
- expect((await store.getallowlist()).map((entry) => entry.fingerprint)).toEqual(["bb22", "aa11"]);
2872
- await store.setallowlistentry({
2873
- fingerprint: "aa11",
2874
- displayname: "Laptop agent",
2875
- namespaces: ["system"],
2876
- grantedat: now,
2877
- history: [{ at: now + 1, actor: "user", change: "Rescoped." }],
2878
- });
2879
- expect((await store.getallowlist()).find((entry) => entry.fingerprint === "aa11")?.namespaces).toEqual(["system"]);
2880
- await store.removeallowlistentry("bb22");
2881
- expect((await store.getallowlist()).map((entry) => entry.fingerprint)).toEqual(["aa11"]);
2882
- expect((await store.getclientidentities())[0]?.displayname).toBe("Laptop agent");
2883
- await store.setstreamchannels([{ id: "c1", clientid: "client1", openedat: now, lastbeatat: now }]);
2884
- await store.setstreamchannels([
2885
- { id: "c1", clientid: "client1", openedat: now, lastbeatat: now, closedat: now + 5 },
2886
- ]);
2887
- expect((await store.getstreamchannels())[0]?.closedat).toBe(now + 5);
2888
- });
2889
-
2890
- it("stores approval gates with their decisions and auth handshake events", async () => {
2891
- const store = new sessionmemory(new fakeadapter());
2892
- const gate = {
2893
- id: "gate-1",
2894
- clientid: "client1",
2895
- tool: "browser.click",
2896
- reason: "Click.",
2897
- params: { stepid: "s1" },
2898
- state: "pending" as const,
2899
- raisedat: now,
2900
- };
2901
- await store.setapproval(gate);
2902
- await store.setapproval({ ...gate, state: "approved", decidedat: now + 1000, actor: "user" });
2903
- const approvals = await store.listapprovals();
2904
- expect(approvals).toHaveLength(1);
2905
- expect(approvals[0]?.state).toBe("approved");
2906
- await store.addapprovalexec({
2907
- requestid: "gate-1",
2908
- decision: "approved",
2909
- actor: "user",
2910
- at: now + 1000,
2911
- latencyms: 1000,
2912
- });
2913
- expect((await store.listapprovalexecs())[0]?.latencyms).toBe(1000);
2914
- await store.addauthhandshake({
2915
- id: "h1",
2916
- clientid: "client1",
2917
- method: "pairingcode",
2918
- outcome: "verified",
2919
- at: now,
2920
- });
2921
- await store.addauthhandshake({ id: "h2", clientid: "new", method: "token", outcome: "refused", at: now + 1 });
2922
- expect((await store.listauthhandshakes()).map((event) => event.outcome)).toEqual(["refused", "verified"]);
2923
- });
2924
- });
2925
-
2926
- describe("sessionmemory agent protocol part three", () => {
2927
- const now = 1_800_000_000_000;
2928
-
2929
- it("stores subscriptions, watches and sampling requests with their provenance", async () => {
2930
- const store = new sessionmemory(new fakeadapter());
2931
- await store.seteventsubscriptions([{ id: "sub1", clientid: "client1", kinds: ["callresult"], createdat: now }]);
2932
- expect((await store.geteventsubscriptions()).map((subscription) => subscription.id)).toEqual(["sub1"]);
2933
- await store.seteventsubscriptions([{ id: "sub2", clientid: "client2", kinds: ["progress"], createdat: now + 1 }]);
2934
- expect((await store.geteventsubscriptions()).map((subscription) => subscription.id)).toEqual(["sub2"]);
2935
- await store.setresourcewatches([
2936
- { id: "watch1", clientid: "client1", resource: "page", baseline: { url: "https://example.com" }, createdat: now },
2937
- ]);
2938
- expect((await store.getresourcewatches())[0]?.baseline).toEqual({ url: "https://example.com" });
2939
- const sampling = {
2940
- id: "sample1",
2941
- clientid: "client1",
2942
- prompt: "Summarize.",
2943
- state: "pending" as const,
2944
- requestedat: now,
2945
- };
2946
- await store.setsamplingrequests([sampling]);
2947
- await store.setsamplingrequests([
2948
- { ...sampling, state: "answered" as const, answeredat: now + 1, answer: "the model answer" },
2949
- ]);
2950
- expect((await store.getsamplingrequests())[0]?.answer).toBe("the model answer");
2951
- });
2952
-
2953
- it("stores idempotency records, rate limit counters and batch outcomes", async () => {
2954
- const store = new sessionmemory(new fakeadapter());
2955
- await store.setidempotencyrecords([
2956
- {
2957
- key: "key-1",
2958
- clientid: "client1",
2959
- tool: "browser.readtext",
2960
- result: { content: "stored", iserror: false },
2961
- createdat: now,
2962
- expiresat: now + 5000,
2963
- },
2964
- ]);
2965
- expect((await store.getidempotencyrecords())[0]?.key).toBe("key-1");
2966
- await store.setcallratelimits([
2967
- { clientid: "client1", windowms: 60_000, budget: 5, windowstartedat: now, used: 3 },
2968
- ]);
2969
- expect((await store.getcallratelimits())[0]?.used).toBe(3);
2970
- const batch = {
2971
- id: "batch1",
2972
- clientid: "client1",
2973
- calls: [{ id: "m1", name: "browser.readtext", params: {} }],
2974
- stoponerror: true,
2975
- state: "running" as const,
2976
- createdat: now,
2977
- outcomes: [],
2978
- };
2979
- await store.setbatchcall(batch);
2980
- await store.setbatchcall({
2981
- ...batch,
2982
- state: "done" as const,
2983
- finishedat: now + 1,
2984
- outcomes: [{ callid: "m1", tool: "browser.readtext", ok: true, at: now + 1 }],
2985
- });
2986
- const stored = await store.getbatchcalls();
2987
- expect(stored).toHaveLength(1);
2988
- expect(stored[0]?.outcomes[0]?.ok).toBe(true);
2989
- });
2990
-
2991
- it("stores call contexts, chunks, progress notices, mocks and the dry run toggle", async () => {
2992
- const store = new sessionmemory(new fakeadapter());
2993
- await store.setcallcontexts([
2994
- { callid: "call1", clientid: "client1", tool: "browser.readtext", state: "inflight", startedat: now, chunks: 0 },
2995
- ]);
2996
- expect((await store.getcallcontexts())[0]?.state).toBe("inflight");
2997
- for (let index = 0; index < 30; index += 1)
2998
- await store.addstreamchunk({
2999
- callid: "call1",
3000
- seq: index + 1,
3001
- content: "x",
3002
- done: index === 29,
3003
- at: now + index,
3004
- });
3005
- expect(await store.getstreamchunks()).toHaveLength(25);
3006
- for (let index = 0; index < 30; index += 1)
3007
- await store.addprogressnotice({ callid: "call1", message: "working", cancellable: true, at: now + index });
3008
- expect(await store.getprogressnotices()).toHaveLength(25);
3009
- await store.settoolmock({
3010
- tool: "browser.readtext",
3011
- result: { content: "canned", iserror: false },
3012
- testcontext: true,
3013
- createdat: now,
3014
- });
3015
- expect((await store.gettoolmocks())[0]?.tool).toBe("browser.readtext");
3016
- await store.removetoolmock("browser.readtext");
3017
- expect(await store.gettoolmocks()).toHaveLength(0);
3018
- await store.setdryruntoggle(true);
3019
- expect(await store.getdryruntoggle()).toBe(true);
3020
- await store.setdryruntoggle(false);
3021
- expect(await store.getdryruntoggle()).toBe(false);
3022
- });
3023
-
3024
- it("returns the audited call log under the requested filters", async () => {
3025
- const store = new sessionmemory(new fakeadapter());
3026
- const one = {
3027
- id: "call1",
3028
- clientid: "client1",
3029
- tool: "browser.readtext",
3030
- origin: "https://example.com",
3031
- ok: true,
3032
- at: now,
3033
- idempotencykey: "key-1",
3034
- };
3035
- const two = {
3036
- id: "call2",
3037
- clientid: "client2",
3038
- tool: "memory.list",
3039
- origin: "https://example.com",
3040
- ok: false,
3041
- code: "consentrefused" as const,
3042
- at: now + 1,
3043
- };
3044
- const three = {
3045
- id: "call3",
3046
- clientid: "client1",
3047
- tool: "browser.click",
3048
- origin: "https://example.com",
3049
- ok: true,
3050
- at: now + 2,
3051
- dryrun: true,
3052
- };
3053
- await store.addtoolcall(one);
3054
- await store.addtoolcall(two);
3055
- await store.addtoolcall(three);
3056
- expect((await store.getcalllog()).map((record) => record.id)).toEqual(["call3", "call2", "call1"]);
3057
- expect((await store.getcalllog({ clientid: "client1" })).map((record) => record.id)).toEqual(["call3", "call1"]);
3058
- expect((await store.getcalllog({ tool: "memory.list" })).map((record) => record.id)).toEqual(["call2"]);
3059
- expect((await store.getcalllog({ ok: true })).map((record) => record.id)).toEqual(["call3", "call1"]);
3060
- expect((await store.getcalllog({ since: now + 1 })).map((record) => record.id)).toEqual(["call3", "call2"]);
3061
- expect((await store.getcalllog({ limit: 1 })).map((record) => record.id)).toEqual(["call3"]);
3062
- expect((await store.getcalllog())[2]?.idempotencykey).toBe("key-1");
3063
- expect((await store.getcalllog())[0]?.dryrun).toBe(true);
3064
- });
3065
- });
3066
-
3067
- describe("sessionmemory llm integration", () => {
3068
- const now = 1_800_000_000_000;
3069
-
3070
- it("stores provider configs with references, never secrets, and the routing tables with revision history", async () => {
3071
- const store = new sessionmemory(new fakeadapter());
3072
- const provider: providerconfig = {
3073
- id: "prov1",
3074
- name: "The gateway",
3075
- endpoint: "https://gateway.example/v1",
3076
- style: "chatcompletions",
3077
- models: ["model-a"],
3078
- status: "available",
3079
- authref: {
3080
- name: "gateway key",
3081
- origins: ["https://gateway.example"],
3082
- header: "authorization",
3083
- storageid: "key-store-1",
3084
- configuredat: now,
3085
- },
3086
- createdat: now,
3087
- };
3088
- await store.setproviders([provider]);
3089
- const stored = (await store.getproviders())[0];
3090
- expect(stored?.authref?.storageid).toBe("key-store-1");
3091
- expect(JSON.stringify(stored)).not.toContain("the key material");
3092
- const route = {
3093
- id: "route1",
3094
- kind: "draftplan",
3095
- providerid: "prov1",
3096
- model: "model-a",
3097
- revision: 2,
3098
- updatedat: now,
3099
- };
3100
- await store.setmodelroutes([route]);
3101
- await store.addmodelrouterevision(route);
3102
- expect((await store.getmodelroutes())[0]?.revision).toBe(2);
3103
- expect((await store.getmodelroutehistory())[0]?.kind).toBe("draftplan");
3104
- });
3105
-
3106
- it("stores every usage record with run and step ids and answers the period totals", async () => {
3107
- const store = new sessionmemory(new fakeadapter());
3108
- const record: usagerecord = {
3109
- id: "u1",
3110
- runid: "run1",
3111
- stepid: "s1",
3112
- providerid: "prov1",
3113
- endpoint: "https://gateway.example/v1",
3114
- model: "model-a",
3115
- prompttokens: 10,
3116
- completiontokens: 5,
3117
- totaltokens: 15,
3118
- cost: 0.01,
3119
- at: now,
3120
- };
3121
- const other: usagerecord = {
3122
- id: "u2",
3123
- runid: "run2",
3124
- providerid: "prov1",
3125
- endpoint: "https://gateway.example/v1",
3126
- model: "model-a",
3127
- prompttokens: 100,
3128
- completiontokens: 50,
3129
- totaltokens: 150,
3130
- cost: 0.1,
3131
- at: now + 1000,
3132
- };
3133
- await store.addusagerecord(record);
3134
- await store.addusagerecord(other);
3135
- expect((await store.getusagerecords())[0]?.id).toBe("u2");
3136
- expect(await store.getusage()).toEqual({
3137
- prompttokens: 110,
3138
- completiontokens: 55,
3139
- totaltokens: 165,
3140
- cost: 0.11,
3141
- calls: 2,
3142
- });
3143
- expect((await store.getusage({ runid: "run1" })).calls).toBe(1);
3144
- expect((await store.getusage({ since: now + 1 })).totaltokens).toBe(150);
3145
- });
3146
-
3147
- it("stores plandrafts, replans and reflectnotes for the audit history", async () => {
3148
- const store = new sessionmemory(new fakeadapter());
3149
- const draft: plandraft = {
3150
- id: "d1",
3151
- goal: "Read the page",
3152
- steps: [{ id: "s1", kind: "observe", summary: "Observe." }],
3153
- openquestions: [],
3154
- providerid: "prov1",
3155
- model: "model-a",
3156
- state: "approved",
3157
- lintfindings: [],
3158
- createdat: now,
3159
- };
3160
- await store.addplandraft(draft);
3161
- expect((await store.getplandrafts())[0]?.goal).toBe("Read the page");
3162
- await store.setplandrafts([{ ...draft, state: "rejected" }]);
3163
- expect((await store.getplandrafts())[0]?.state).toBe("rejected");
3164
- const replan: replanrecord = {
3165
- id: "r1",
3166
- draftid: "d1",
3167
- completedstepids: ["s1"],
3168
- failedstepids: [],
3169
- tail: [{ id: "t1", kind: "reload", summary: "Reload.", freshreview: true }],
3170
- reason: "The step timed out.",
3171
- providerid: "prov1",
3172
- model: "model-a",
3173
- state: "pending",
3174
- createdat: now,
3175
- };
3176
- await store.addreplan(replan);
3177
- expect((await store.getreplans())[0]?.tail[0]?.freshreview).toBe(true);
3178
- const note: reflectnote = {
3179
- id: "n1",
3180
- runid: "run1",
3181
- stepid: "s1",
3182
- outcome: "The step landed.",
3183
- lesson: "Wait before the click.",
3184
- advice: "Add a settle window.",
3185
- providerid: "prov1",
3186
- model: "model-a",
3187
- createdat: now,
3188
- };
3189
- await store.addreflectnote(note);
3190
- expect((await store.getreflectnotes())[0]?.lesson).toMatch(/Wait before the click/);
3191
- });
3192
-
3193
- it("stores prompt template versions with change notes and the cost budgets", async () => {
3194
- const store = new sessionmemory(new fakeadapter());
3195
- const template: prompttemplate = {
3196
- id: "t1",
3197
- name: "pagesummary",
3198
- body: "Summarize {{url}}.",
3199
- variables: ["url"],
3200
- version: 1,
3201
- notes: "first version",
3202
- createdat: now,
3203
- };
3204
- await store.setprompttemplates([template]);
3205
- expect((await store.getprompttemplates())[0]?.notes).toBe("first version");
3206
- const budget: costbudget = { maxtokens: 5000, configuredat: now };
3207
- await store.setcostbudget(budget);
3208
- expect((await store.getcostbudget())?.maxtokens).toBe(5000);
3209
- await store.setcostbudget({ runid: "run1", maxcost: 1, currency: "usd", configuredat: now });
3210
- expect((await store.getcostbudget("run1"))?.maxcost).toBe(1);
3211
- expect((await store.getcostbudget("run2"))?.maxtokens).toBe(5000);
3212
- });
3213
-
3214
- it("stores the parsed command and the guard refusal notices with their reasons", async () => {
3215
- const store = new sessionmemory(new fakeadapter());
3216
- const parse = {
3217
- text: "open the docs",
3218
- intent: "navigate" as const,
3219
- entities: [{ name: "url", value: "https://docs.example" }],
3220
- confidence: 0.9,
3221
- model: "model-a",
3222
- providerid: "prov1",
3223
- parsedat: now,
3224
- };
3225
- await store.setcommandparse(parse);
3226
- expect((await store.getcommandparse())?.intent).toBe("navigate");
3227
- await store.addguardnotice({
3228
- raw: "nope",
3229
- verdict: "invalid",
3230
- reason: "The model answer is not json.",
3231
- attempts: 3,
3232
- });
3233
- await store.addguardnotice({ raw: "{}", parsed: {}, verdict: "valid", attempts: 1 });
3234
- const notices = await store.getguardnotices();
3235
- expect(notices).toHaveLength(1);
3236
- expect(notices[0]?.reason).toMatch(/not json/i);
3237
- });
3238
- });
3239
-
3240
- describe("sessionmemory swarm persistence", () => {
3241
- const now = 1_800_000_000_000;
3242
- it("stores the agent identities, the task queue and the blackboard as first class records", async () => {
3243
- const store = new sessionmemory(new fakeadapter());
3244
- await store.setagents([
3245
- { id: "a1", name: "Scout", role: "worker", depth: 0, state: "active", tabid: 7, registeredat: now },
3246
- ]);
3247
- expect((await store.getagents())[0]).toMatchObject({ name: "Scout", tabid: 7, role: "worker" });
3248
- await store.settaskqueue({
3249
- lanes: ["extraction"],
3250
- priorities: [1],
3251
- completionpolicy: "all",
3252
- items: [
3253
- { id: "t1", lane: "extraction", priority: 1, payload: "Read the table", state: "queued", enqueuedat: now },
3254
- ],
3255
- claims: [],
3256
- });
3257
- expect((await store.gettaskqueue())?.items).toHaveLength(1);
3258
- await store.setblackboard({
3259
- sections: ["goals", "facts", "findings", "scratch"],
3260
- entries: [
3261
- {
3262
- id: "e1",
3263
- key: "goal",
3264
- valuekind: "text",
3265
- value: "Extract",
3266
- author: "user",
3267
- section: "goals",
3268
- consentclass: "read",
3269
- postedat: now,
3270
- },
3271
- ],
3272
- });
3273
- expect((await store.getblackboard())?.entries[0]?.key).toBe("goal");
3274
- await store.setkillswitch({ engaged: true, engagedat: now, reason: "Halted." });
3275
- expect((await store.getkillswitch())?.engaged).toBe(true);
3276
- });
3277
-
3278
- it("stores the lessons learned of the fleet with one row per lesson id", async () => {
3279
- /* the lessons store of the 1.1.96 multi agent certification: a rewritten lesson replaces its row instead of duplicating it, so the store deduplicates and the reuse count survives the rewrite */
3280
- const store = new sessionmemory(new fakeadapter());
3281
- const first: lessonrecord = {
3282
- id: "l1",
3283
- agentid: "w1",
3284
- finding: "The pricing table loads only after the hero settles.",
3285
- origin: "https://example.com",
3286
- reusecount: 0,
3287
- recordedat: now,
3288
- };
3289
- const second: lessonrecord = {
3290
- id: "l2",
3291
- agentid: "w2",
3292
- finding: "The login form refuses pasted passwords.",
3293
- origin: "https://shop.example",
3294
- reusecount: 0,
3295
- recordedat: now,
3296
- };
3297
- await store.setlesson(first);
3298
- await store.setlesson(second);
3299
- expect((await store.getlessons()).map((lesson) => lesson.id).sort()).toEqual(["l1", "l2"]);
3300
- const rewritten: lessonrecord = { ...first, reusecount: 1, lastusedat: now + 1_000 };
3301
- await store.setlesson(rewritten);
3302
- const stored = await store.getlessons();
3303
- expect(stored).toHaveLength(2);
3304
- expect(stored.find((lesson) => lesson.id === "l1")).toMatchObject({ reusecount: 1, lastusedat: now + 1_000 });
3305
- expect(stored.find((lesson) => lesson.id === "l1")?.finding).toBe(
3306
- "The pricing table loads only after the hero settles.",
3307
- );
3308
- });
3309
-
3310
- it("stores the mailbox contents with the user configured retention", async () => {
3311
- const store = new sessionmemory(new fakeadapter());
3312
- await store.setmailboxes([
3313
- {
3314
- agentid: "a1",
3315
- inbox: [
3316
- { id: "m1", senderid: "a2", recipient: "a1", routing: "direct", payload: "One", sentat: now },
3317
- { id: "m2", senderid: "a2", recipient: "a1", routing: "direct", payload: "Two", sentat: now + 1 },
3318
- ],
3319
- outbox: [],
3320
- unread: 2,
3321
- },
3322
- ]);
3323
- expect((await store.getmailboxes())[0]?.inbox).toHaveLength(2);
3324
- await store.setsettings({ mailboxretention: 1 });
3325
- await store.setmailboxes((await store.getmailboxes()).map((mailbox) => ({ ...mailbox })));
3326
- expect((await store.getmailboxes())[0]?.inbox).toHaveLength(1);
3327
- });
3328
-
3329
- it("stores the spawn history, the per agent usage and the lifecycle events", async () => {
3330
- const store = new sessionmemory(new fakeadapter());
3331
- await store.addspawnrecord({ id: "s1", parentid: "a1", childid: "a2", role: "planner", depth: 1, at: now });
3332
- await store.addspawnrecord({ id: "s2", parentid: "a2", childid: "a3", role: "worker", depth: 2, at: now + 1 });
3333
- expect((await store.getspawnrecords()).map((record) => record.childid)).toEqual(["a3", "a2"]);
3334
- await store.setagentusage([{ agentid: "a1", tokens: 120, cost: 0.02, steps: 3, updatedat: now }]);
3335
- expect((await store.getagentusage())[0]).toMatchObject({ tokens: 120, steps: 3 });
3336
- await store.addagentevent({
3337
- id: "e1",
3338
- kind: "register",
3339
- agentid: "a1",
3340
- summary: "The user registered the agent Scout.",
3341
- at: now,
3342
- });
3343
- expect((await store.getagentevents())[0]?.kind).toBe("register");
3344
- });
3345
-
3346
- it("returns the swarm overview at a glance from the stored records", async () => {
3347
- const store = new sessionmemory(new fakeadapter());
3348
- await store.setagents([
3349
- { id: "a1", name: "Scout", role: "worker", depth: 0, state: "active", registeredat: now },
3350
- { id: "a2", name: "Scribe", role: "planner", depth: 0, state: "paused", registeredat: now },
3351
- ]);
3352
- await store.settaskqueue({
3353
- lanes: ["extraction"],
3354
- priorities: [],
3355
- completionpolicy: "all",
3356
- items: [
3357
- { id: "t1", lane: "extraction", priority: 1, payload: "Read the table", state: "claimed", enqueuedat: now },
3358
- { id: "t2", lane: "extraction", priority: 1, payload: "Read the footer", state: "queued", enqueuedat: now },
3359
- ],
3360
- claims: [{ agentid: "a1", taskid: "t1", claimedat: now, heartbeatat: now }],
3361
- });
3362
- await store.setmailboxes([
3363
- {
3364
- agentid: "a2",
3365
- inbox: [{ id: "m1", senderid: "a1", recipient: "a2", routing: "direct", payload: "Hi.", sentat: now }],
3366
- outbox: [],
3367
- unread: 1,
3368
- },
3369
- ]);
3370
- const overview = await store.swarmoverview();
3371
- expect(overview).toMatchObject({
3372
- agents: 2,
3373
- active: 1,
3374
- paused: 1,
3375
- tasks: 2,
3376
- queued: 1,
3377
- claimed: 1,
3378
- messages: 1,
3379
- unread: 1,
3380
- });
3381
- const empty = await new sessionmemory(new fakeadapter()).swarmoverview();
3382
- expect(empty).toMatchObject({ agents: 0, tasks: 0, messages: 0 });
3383
- });
3384
- });
3385
-
3386
- describe("sessionmemory orchestration persistence", () => {
3387
- const now = 1_800_000_000_000;
3388
- it("stores the topology, splits, reviews and verifier checks of the 1.1.59 family", async () => {
3389
- const store = new sessionmemory(new fakeadapter());
3390
- await store.settopology({
3391
- id: "top1",
3392
- leaderid: "a1",
3393
- workerids: ["a2"],
3394
- criticids: ["a3"],
3395
- verifierids: ["a4"],
3396
- assignments: [{ workerid: "a2", taskid: "t1", slice: "Read the table", assignedat: now }],
3397
- rule: { kind: "first" },
3398
- electedat: now,
3399
- });
3400
- expect((await store.gettopology())?.leaderid).toBe("a1");
3401
- await store.setplannersplits([
3402
- {
3403
- id: "s1",
3404
- planownerid: "a1",
3405
- runownerid: "a2",
3406
- taskid: "t1",
3407
- stepreports: [{ stepid: "step1", outcome: "done", detail: "Done.", reportedat: now }],
3408
- splitat: now,
3409
- },
3410
- ]);
3411
- expect((await store.getplannersplits())[0]?.stepreports).toHaveLength(1);
3412
- await store.addcriticreview({
3413
- id: "r1",
3414
- reviewerid: "a3",
3415
- subjectagentid: "a2",
3416
- verdict: "changes",
3417
- issues: [],
3418
- requiredchanges: ["Re-read the footer."],
3419
- reviewedat: now,
3420
- });
3421
- expect((await store.getcriticreviews())[0]?.verdict).toBe("changes");
3422
- await store.addverifiercheck({
3423
- id: "v1",
3424
- verifierid: "a4",
3425
- claimagentid: "a2",
3426
- claim: "The table holds 42 rows.",
3427
- method: "re-read",
3428
- outcome: "pass",
3429
- checkedat: now,
3430
- });
3431
- expect((await store.getverifierchecks())[0]?.outcome).toBe("pass");
3432
- await store.setreviewrequests([
3433
- {
3434
- id: "rq1",
3435
- fromagentid: "a2",
3436
- toagentid: "a3",
3437
- subject: "The output",
3438
- payload: "The rows",
3439
- state: "open",
3440
- requestedat: now,
3441
- },
3442
- ]);
3443
- expect((await store.getreviewrequests())[0]?.state).toBe("open");
3444
- });
3445
-
3446
- it("stores the handoff log with the resumed state, the locks and the conflict scans", async () => {
3447
- const store = new sessionmemory(new fakeadapter());
3448
- await store.addhandoff({
3449
- id: "h1",
3450
- fromagentid: "a1",
3451
- toagentid: "a2",
3452
- tabid: 7,
3453
- taskstate: "Halfway.",
3454
- state: "prepared",
3455
- createdat: now,
3456
- });
3457
- await store.updatehandoff({
3458
- id: "h1",
3459
- fromagentid: "a1",
3460
- toagentid: "a2",
3461
- tabid: 7,
3462
- taskstate: "Halfway.",
3463
- state: "resumed",
3464
- createdat: now,
3465
- transferredat: now + 1,
3466
- resumedat: now + 2,
3467
- });
3468
- const handoffs = await store.gethandoffs();
3469
- expect(handoffs).toHaveLength(1);
3470
- expect(handoffs[0]).toMatchObject({ state: "resumed", resumedat: now + 2 });
3471
- await store.setlocks([
3472
- {
3473
- key: "https://example.com|#form",
3474
- holder: "a1",
3475
- kind: "exclusive",
3476
- origin: "https://example.com",
3477
- selector: "#form",
3478
- acquiredat: now,
3479
- expiresat: now + 1000,
3480
- },
3481
- ]);
3482
- expect((await store.getlocks())[0]?.holder).toBe("a1");
3483
- await store.addconflictscan({
3484
- id: "s1",
3485
- writers: [{ agentid: "a1", origin: "https://example.com", selector: "#form" }],
3486
- overlaps: [],
3487
- suggestedorder: [],
3488
- clean: true,
3489
- scannedat: now,
3490
- });
3491
- expect((await store.getconflictscans())[0]?.clean).toBe(true);
3492
- });
3493
-
3494
- it("stores the merged report with provenance, the board snapshots with retention, the escalations, the consensus rounds, the shared costs and the interleaved timeline", async () => {
3495
- const store = new sessionmemory(new fakeadapter());
3496
- await store.setreport({
3497
- id: "rep1",
3498
- title: "The extraction",
3499
- sections: [
3500
- {
3501
- title: "Task t1",
3502
- entries: [{ id: "m1", agentid: "a1", taskid: "t1", key: "rowcount", value: "42", mergedat: now }],
3503
- sources: ["a1"],
3504
- },
3505
- ],
3506
- sources: ["a1"],
3507
- createdat: now,
3508
- });
3509
- expect((await store.getreport())?.sections[0]?.entries[0]?.agentid).toBe("a1");
3510
- await store.addboardsnapshot({ id: "b1", builtat: now, lanes: [] });
3511
- await store.addboardsnapshot({ id: "b2", builtat: now + 1, lanes: [] });
3512
- expect((await store.getboardsnapshots()).map((board) => board.id)).toEqual(["b2", "b1"]);
3513
- await store.setsettings({ boardretention: 1 });
3514
- await store.addboardsnapshot({ id: "b3", builtat: now + 2, lanes: [] });
3515
- expect(await store.getboardsnapshots()).toHaveLength(1);
3516
- await store.addescalation({
3517
- id: "e1",
3518
- agentid: "a1",
3519
- subject: "Which origin",
3520
- context: "Both hold half.",
3521
- state: "open",
3522
- raisedat: now,
3523
- });
3524
- await store.updateescalation({
3525
- id: "e1",
3526
- agentid: "a1",
3527
- subject: "Which origin",
3528
- context: "Both hold half.",
3529
- state: "decided",
3530
- decision: "The second origin.",
3531
- raisedat: now,
3532
- decidedat: now + 1,
3533
- });
3534
- expect((await store.getescalations())[0]?.decision).toBe("The second origin.");
3535
- await store.setconsensusround({
3536
- id: "c1",
3537
- subject: "The lane order",
3538
- votes: [],
3539
- quorum: 2,
3540
- state: "open",
3541
- openedat: now,
3542
- });
3543
- await store.setconsensusround({
3544
- id: "c1",
3545
- subject: "The lane order",
3546
- votes: [{ agentid: "a1", vote: "yes", votedat: now + 1 }],
3547
- quorum: 2,
3548
- state: "open",
3549
- openedat: now,
3550
- });
3551
- const rounds = await store.getconsensusrounds();
3552
- expect(rounds).toHaveLength(1);
3553
- expect(rounds[0]?.votes).toHaveLength(1);
3554
- await store.setconsensusround({ id: "c2", subject: "Abort", votes: [], quorum: 1, state: "open", openedat: now });
3555
- expect(await store.getconsensusrounds()).toHaveLength(2);
3556
- await store.addswarmcost({ agents: 2, tokens: 150, cost: 0.75, steps: 5, computedat: now });
3557
- expect((await store.getswarmcosts())[0]).toMatchObject({ tokens: 150, steps: 5 });
3558
- await store.addswarmaction({ id: "t1", kind: "claim", agentid: "a1", summary: "The claim.", at: now });
3559
- await store.addswarmaction({ id: "t2", kind: "lock", agentid: "a2", summary: "The lock.", at: now + 1 });
3560
- await store.addswarmaction({ id: "t3", kind: "merge", summary: "The merge.", at: now + 2 });
3561
- expect((await store.getswarmtimeline()).map((action) => action.id)).toEqual(["t1", "t2", "t3"]);
3562
- expect((await store.getswarmtimeline({ agentid: "a1" })).map((action) => action.id)).toEqual(["t1"]);
3563
- expect((await store.getswarmtimeline({ kind: "lock" })).map((action) => action.id)).toEqual(["t2"]);
3564
- expect((await store.getswarmtimeline({ since: now + 1 })).map((action) => action.id)).toEqual(["t2", "t3"]);
3565
- });
3566
- });
3567
-
3568
- describe("execution environment persistence", () => {
3569
- it("joins the environment grants to the session record and reads them back", async () => {
3570
- const store = new sessionmemory(new fakeadapter());
3571
- await store.setsession({ id: "s1", tabid: 1, origin: "https://example.com", startedat: 1, expiresat: 2 });
3572
- expect(await store.getenvironmentgrants()).toBeUndefined();
3573
- await store.setenvironmentgrants(["pagecontext", "isolatedworld"]);
3574
- expect(await store.getenvironmentgrants()).toEqual(["pagecontext", "isolatedworld"]);
3575
- expect((await store.getsession())?.environmentgrants).toEqual(["pagecontext", "isolatedworld"]);
3576
- await expect(store.setenvironmentgrants([])).resolves.toBeUndefined();
3577
- });
3578
-
3579
- it("seals the run state per profile so parallel profiles never share it and tampered seals never load", async () => {
3580
- const store = new sessionmemory(new fakeadapter());
3581
- const state = {
3582
- runid: "run1",
3583
- sessionid: "s1",
3584
- planid: "p1",
3585
- profileid: "profile1",
3586
- state: "active" as const,
3587
- urlhistory: [],
3588
- environments: {},
3589
- turnarounds: {},
3590
- keepalive: {
3591
- runid: "run1",
3592
- sessionid: "s1",
3593
- state: "active" as const,
3594
- startedat: 1,
3595
- interval: 30_000,
3596
- beats: 1,
3597
- lastbeatat: 2,
3598
- portopen: true,
3599
- events: [],
3600
- },
3601
- updatedat: 2,
3602
- };
3603
- await store.setrunstate("profile1", state);
3604
- await store.setrunstate("profile2", { ...state, profileid: "profile2", runid: "run2" });
3605
- expect((await store.getrunstate("profile1"))?.runid).toBe("run1");
3606
- expect((await store.getrunstate("profile2"))?.runid).toBe("run2");
3607
- expect((await store.listrunstates()).map((entry) => entry.runid).sort()).toEqual(["run1", "run2"]);
3608
- await store.removerunstate("profile2");
3609
- expect(await store.getrunstate("profile2")).toBeUndefined();
3610
- expect((await store.listrunstates()).map((entry) => entry.runid)).toEqual(["run1"]);
3611
- });
3612
-
3613
- it("expires the stale stopped run states past the user configured window while the keepalive summaries survive", async () => {
3614
- const store = new sessionmemory(new fakeadapter());
3615
- const stopped = {
3616
- runid: "run1",
3617
- sessionid: "s1",
3618
- planid: "p1",
3619
- profileid: "default",
3620
- state: "completed" as const,
3621
- urlhistory: [{ url: "https://example.com/a", stepid: "s1", at: 1 }],
3622
- environments: { s1: "pagecontext" as const },
3623
- turnarounds: {},
3624
- keepalive: {
3625
- runid: "run1",
3626
- sessionid: "s1",
3627
- state: "stopped" as const,
3628
- startedat: 1,
3629
- interval: 30_000,
3630
- beats: 4,
3631
- lastbeatat: 2,
3632
- portopen: false,
3633
- events: [],
3634
- stoppedat: 3,
3635
- },
3636
- updatedat: 3,
3637
- };
3638
- const active = {
3639
- ...stopped,
3640
- runid: "run2",
3641
- keepalive: { ...stopped.keepalive, runid: "run2", state: "active" as const, portopen: true },
3642
- updatedat: 100,
3643
- };
3644
- await store.setrunstate("default", stopped);
3645
- await store.setrunstate("live", active);
3646
- const kept = await store.expirerunstates(1_000, 5_000);
3647
- expect(kept.map((entry) => entry.runid)).toEqual(["run2"]);
3648
- const summary = await store.getrunstate("default");
3649
- expect(summary?.state).toBe("expired");
3650
- expect(summary?.urlhistory).toEqual([]);
3651
- expect(summary?.keepalive.beats).toBe(4);
3652
- expect(await store.expirerunstates(undefined, 9_000)).toHaveLength(2);
3653
- });
3654
-
3655
- it("stores worker events with provenance, the offscreen registry, sandbox renders and the run locks", async () => {
3656
- const store = new sessionmemory(new fakeadapter());
3657
- await store.addworkerevent({
3658
- id: "w1",
3659
- runid: "run1",
3660
- kind: "spawn",
3661
- workers: 2,
3662
- reason: "Two parses waited.",
3663
- provenance: { origin: "https://example.com", stepid: "s1", environment: "offscreenworker" },
3664
- at: 1,
3665
- });
3666
- await store.addworkerevent({
3667
- id: "w2",
3668
- runid: "run1",
3669
- kind: "teardown",
3670
- workers: 0,
3671
- reason: "The run completed.",
3672
- provenance: { origin: "https://example.com", stepid: "s1", environment: "offscreenworker" },
3673
- at: 2,
3674
- });
3675
- expect((await store.getworkerevents()).map((event) => event.kind)).toEqual(["teardown", "spawn"]);
3676
- await store.addoffscreenentry({
3677
- document: "offscreen.html",
3678
- runid: "run1",
3679
- reasons: ["DOM_PARSER", "WORKERS"],
3680
- justification: "Heavy parsing of reviewed snapshots.",
3681
- createdat: 1,
3682
- });
3683
- await store.updateoffscreenentry({
3684
- document: "offscreen.html",
3685
- runid: "run1",
3686
- reasons: ["DOM_PARSER", "WORKERS"],
3687
- justification: "Heavy parsing of reviewed snapshots.",
3688
- createdat: 1,
3689
- closedat: 9,
3690
- });
3691
- expect((await store.getoffscreenentries())[0]?.closedat).toBe(9);
3692
- await store.addsandboxrender({
3693
- id: "r1",
3694
- nonce: "abc123def456ghi7",
3695
- markup: "<p>ok</p>",
3696
- sourceorigin: "https://example.com",
3697
- stepid: "s1",
3698
- renderedat: 1,
3699
- });
3700
- expect((await store.getsandboxrenders())[0]?.nonce).toBe("abc123def456ghi7");
3701
- await store.setrunlocks([{ sessionid: "s1", runid: "run1", holder: "executor", acquiredat: 1 }]);
3702
- expect((await store.getrunlocks())[0]?.runid).toBe("run1");
3703
- });
3704
-
3705
- it("tracks the storage quota usage of the run state and exports every run state as one record", async () => {
3706
- const store = new sessionmemory(new fakeadapter());
3707
- await store.setsettings({ runstatebytes: 500 });
3708
- await store.trackrunstatequota(1_200);
3709
- const quota = await store.getrunstatequota();
3710
- expect(quota).toMatchObject({ used: 1_200, ceiling: 500 });
3711
- const state = {
3712
- runid: "run1",
3713
- sessionid: "s1",
3714
- planid: "p1",
3715
- profileid: "default",
3716
- state: "active" as const,
3717
- urlhistory: [],
3718
- environments: { s1: "offscreenworker" as const },
3719
- turnarounds: { s1: 8 },
3720
- keepalive: {
3721
- runid: "run1",
3722
- sessionid: "s1",
3723
- state: "active" as const,
3724
- startedat: 1,
3725
- interval: 30_000,
3726
- beats: 2,
3727
- lastbeatat: 2,
3728
- portopen: true,
3729
- events: [],
3730
- },
3731
- updatedat: 2,
3732
- };
3733
- await store.setrunstate("default", state);
3734
- const exported = await store.exportrunstates();
3735
- expect(exported).toMatchObject({ runs: 1, environments: 1, offloaded: 1, beats: 2 });
3736
- });
3737
-
3738
- it("stores the automation allowlist, the origin profiles, the consent windows and the class consents of the security family", async () => {
3739
- const store = new sessionmemory(new fakeadapter());
3740
- await store.addallowlistorigin({ origin: "https://example.com", profileid: "default", grantedat: 1 });
3741
- await store.addallowlistorigin({ origin: "https://example.com", profileid: "default", grantedat: 2 });
3742
- await store.addallowlistorigin({ origin: "https://other.example", profileid: "default", grantedat: 3 });
3743
- expect((await store.getautomationallowlist()).map((entry) => entry.origin)).toEqual([
3744
- "https://example.com",
3745
- "https://other.example",
3746
- ]);
3747
- await store.removeallowlistorigin("https://other.example", "default");
3748
- expect(await store.getautomationallowlist()).toHaveLength(1);
3749
- await store.saveoriginprofile({
3750
- profileid: "p1",
3751
- origin: "https://example.com",
3752
- grants: ["fillcard" as const],
3753
- denials: [],
3754
- createdat: 1,
3755
- updatedat: 1,
3756
- });
3757
- await store.saveoriginprofile({
3758
- profileid: "p1",
3759
- origin: "https://example.com",
3760
- grants: [],
3761
- denials: ["fillcard" as const],
3762
- createdat: 1,
3763
- updatedat: 2,
3764
- });
3765
- await store.saveoriginprofile({
3766
- profileid: "p2",
3767
- origin: "https://other.example",
3768
- grants: [],
3769
- denials: [],
3770
- createdat: 1,
3771
- updatedat: 1,
3772
- });
3773
- const profiles = await store.getoriginprofiles();
3774
- expect(profiles).toHaveLength(2);
3775
- expect(profiles.find((profile) => profile.origin === "https://example.com")?.denials).toEqual(["fillcard"]);
3776
- await store.setconsentwindows([
3777
- {
3778
- id: "w1",
3779
- sessionid: "s1",
3780
- origin: "https://example.com",
3781
- startedat: 1,
3782
- duration: 10,
3783
- expiresat: 11,
3784
- boundary: "10 milliseconds",
3785
- kinds: [],
3786
- state: "active",
3787
- },
3788
- ]);
3789
- const expired = await store.expireconsentwindows(12);
3790
- expect(expired[0]?.state).toBe("closed");
3791
- expect(expired[0]?.closedat).toBe(12);
3792
- await store.addclassconsent({ id: "c1", origin: "https://example.com", sensitiveclass: "payment", grantedat: 1 });
3793
- await store.addclassconsent({ id: "c2", origin: "https://example.com", sensitiveclass: "payment", grantedat: 2 });
3794
- expect((await store.getclassconsents()).filter((consent) => consent.sensitiveclass === "payment")).toHaveLength(1);
3795
- await store.addrevocation({
3796
- id: "r1",
3797
- sessionid: "s1",
3798
- runid: "run1",
3799
- haltedstepids: ["s1", "s2"],
3800
- actor: "user",
3801
- reason: "The user revoked the consent mid run.",
3802
- at: 3,
3803
- });
3804
- expect((await store.getrevocations())[0]?.haltedstepids).toEqual(["s1", "s2"]);
3805
- });
3806
-
3807
- it("stores the mask rules per origin with add and remove", async () => {
3808
- const store = new sessionmemory(new fakeadapter());
3809
- await store.addmaskrule({ id: "m1", shapes: ["iban"], createdat: 1 });
3810
- await store.addmaskrule({ id: "m2", origin: "https://example.com", shapes: ["routing"], createdat: 2 });
3811
- expect((await store.getmaskrules()).map((rule) => rule.id)).toEqual(["m1", "m2"]);
3812
- await store.removemaskrule("m1");
3813
- expect((await store.getmaskrules()).map((rule) => rule.id)).toEqual(["m2"]);
3814
- });
3815
-
3816
- it("stores the immutable run log and exports only the verified chain", async () => {
3817
- const store = new sessionmemory(new fakeadapter());
3818
- let log = { runid: "run1", sessionid: "s1", entries: [] as never[], updatedat: 1 } as Parameters<
3819
- sessionmemory["setimmutablelog"]
3820
- >[0];
3821
- const { appendlogentry, sealrunlog } = await import("../security.js");
3822
- log = await appendlogentry({
3823
- log,
3824
- kind: "grant",
3825
- summary: "The session started.",
3826
- origin: "https://example.com",
3827
- at: 2,
3828
- });
3829
- log = await appendlogentry({
3830
- log,
3831
- kind: "step",
3832
- summary: "The click step completed.",
3833
- origin: "https://example.com",
3834
- stepid: "s1",
3835
- at: 3,
3836
- });
3837
- await store.trackimmutablelog("run1");
3838
- await store.setimmutablelog(log);
3839
- const stored = await store.getimmutablelog("run1");
3840
- expect(stored?.entries).toHaveLength(2);
3841
- expect(await store.listimmutablelogs()).toHaveLength(1);
3842
- const sealed = await sealrunlog(log, 4);
3843
- await store.setimmutablelog(sealed.log);
3844
- const exported = await store.exportverifiedrunlog("run1");
3845
- expect(exported.chainvalid).toBe(true);
3846
- expect(exported.entries).toBe(2);
3847
- expect(exported.sealhash).toMatch(/^[0-9a-f]{64}$/);
3848
- await expect(store.exportverifiedrunlog("missing")).rejects.toThrow(/no run log exists/i);
3849
- const expired = await store.expireimmutablelogs(10, 100);
3850
- expect(expired).toHaveLength(0);
3851
- const pruned = await store.getimmutablelog("run1");
3852
- expect(pruned?.entries).toEqual([]);
3853
- expect(pruned?.seal?.sealhash.current).toMatch(/^[0-9a-f]{64}$/);
3854
- });
3855
- });
3856
-
3857
- describe("sessionmemory security part two", () => {
3858
- it("stores secretvault metadata with labels and scopes only and never values", async () => {
3859
- const store = new sessionmemory(new fakeadapter());
3860
- const entry = {
3861
- vaultid: "v1",
3862
- label: "Bank login",
3863
- scope: "https://bank.example",
3864
- profileid: "profile",
3865
- provenance: "user" as const,
3866
- algorithm: "sha-256" as const,
3867
- digest: "sha256:abc",
3868
- createdat: 1,
3869
- };
3870
- await store.addsecret(entry);
3871
- await store.addsecret({ ...entry, label: "duplicate id keeps first" });
3872
- expect(await store.getsecretvault()).toHaveLength(1);
3873
- await store.stampsecretuse("v1", 9);
3874
- expect((await store.getsecretvault())[0]?.lastusedat).toBe(9);
3875
- await store.removesecret("v1");
3876
- expect(await store.getsecretvault()).toEqual([]);
3877
- });
3878
-
3879
- it("stores connectallow entries with their senders and drops them on removal", async () => {
3880
- const store = new sessionmemory(new fakeadapter());
3881
- await store.addconnectallow({ senderid: "sender", displayname: "Bridge", addedat: 1 });
3882
- await store.addconnectallow({ senderid: "sender", displayname: "duplicate keeps first", addedat: 2 });
3883
- expect((await store.getconnectallow())[0]?.displayname).toBe("Bridge");
3884
- await store.removeconnectallow("sender");
3885
- expect(await store.getconnectallow()).toEqual([]);
3886
- });
3887
-
3888
- it("stores ratelimit bucket state per origin and per session", async () => {
3889
- const store = new sessionmemory(new fakeadapter());
3890
- await store.saveratelimitbucket({
3891
- origin: "https://example.com",
3892
- sessionid: "s",
3893
- limit: 5,
3894
- window: 1000,
3895
- used: 1,
3896
- windowstartedat: 1,
3897
- resetsat: 1001,
3898
- });
3899
- await store.saveratelimitbucket({
3900
- origin: "https://example.com",
3901
- sessionid: "s",
3902
- limit: 5,
3903
- window: 1000,
3904
- used: 2,
3905
- windowstartedat: 1,
3906
- resetsat: 1001,
3907
- });
3908
- expect(await store.getratelimitbuckets()).toHaveLength(1);
3909
- await store.saveratelimitbucket({
3910
- origin: "https://other.example",
3911
- sessionid: "s",
3912
- limit: 5,
3913
- window: 1000,
3914
- used: 0,
3915
- windowstartedat: 1,
3916
- resetsat: 1001,
3917
- });
3918
- expect(await store.getratelimitbuckets()).toHaveLength(2);
3919
- await store.removeratelimitbucket("https://example.com", "s");
3920
- expect((await store.getratelimitbuckets()).map((bucket) => bucket.origin)).toEqual(["https://other.example"]);
3921
- });
3922
-
3923
- it("stores confirm gates with their resolution events and human action provenance", async () => {
3924
- const store = new sessionmemory(new fakeadapter());
3925
- const gate = {
3926
- gateid: "g1",
3927
- kind: "confirmpay" as const,
3928
- stepid: "step",
3929
- runid: "run",
3930
- origin: "https://shop.example",
3931
- payload: { amount: "10.00" },
3932
- state: "open" as const,
3933
- openedat: 1,
3934
- };
3935
- await store.savegate(gate);
3936
- await store.savegate({ ...gate, state: "resolved", resolvedat: 3, actor: "user" });
3937
- expect((await store.getgates())[0]?.state).toBe("resolved");
3938
- await store.savegate({ ...gate, gateid: "g2", kind: "confirmdelete" as const });
3939
- expect((await store.getgates()).map((candidate) => candidate.gateid)).toEqual(["g2", "g1"]);
3940
- await store.addgateresolution({
3941
- gateid: "g1",
3942
- kind: "confirmpay",
3943
- stepid: "step",
3944
- decision: "resolved",
3945
- actor: "user",
3946
- at: 3,
3947
- });
3948
- expect((await store.getgateresolutions())[0]).toMatchObject({ gateid: "g1", actor: "user" });
3949
- });
3950
-
3951
- it("stores redactshot regions per origin and page template", async () => {
3952
- const store = new sessionmemory(new fakeadapter());
3953
- const region = {
3954
- id: "r1",
3955
- origin: "https://bank.example",
3956
- template: "login",
3957
- x: 0,
3958
- y: 0,
3959
- width: 10,
3960
- height: 10,
3961
- reason: "the card field",
3962
- source: "userdrawn" as const,
3963
- createdat: 1,
3964
- };
3965
- await store.addredactregion(region);
3966
- expect(await store.getredactregions()).toHaveLength(1);
3967
- await store.removeredactregion("r1");
3968
- expect(await store.getredactregions()).toEqual([]);
3969
- });
3970
-
3971
- it("stores phishguard verdicts and expires the ones past the freshness window", async () => {
3972
- const store = new sessionmemory(new fakeadapter());
3973
- await store.addphishverdict({
3974
- origin: "https://pay.example.com",
3975
- matchedorigin: "https://pay.example.com",
3976
- distance: 0.2,
3977
- threshold: 0.4,
3978
- blocked: false,
3979
- reason: "under the threshold",
3980
- at: 1,
3981
- });
3982
- await store.addphishverdict({
3983
- origin: "https://pay.example.com",
3984
- distance: 0.3,
3985
- threshold: 0.4,
3986
- blocked: true,
3987
- reason: "crossed the threshold",
3988
- at: 5,
3989
- });
3990
- expect(await store.getphishverdicts()).toHaveLength(1);
3991
- expect((await store.getphishverdicts())[0]?.at).toBe(5);
3992
- expect(await store.expirephishverdicts(undefined, 100)).toHaveLength(1);
3993
- expect(await store.expirephishverdicts(10, 100)).toEqual([]);
3994
- });
3995
-
3996
- it("stores permdiff records of each installed version with the last permission set", async () => {
3997
- const store = new sessionmemory(new fakeadapter());
3998
- await store.addpermdiff({
3999
- fromversion: "1.1.61",
4000
- toversion: "1.1.62",
4001
- added: ["optional:downloads"],
4002
- removed: [],
4003
- computedat: 1,
4004
- });
4005
- expect((await store.getpermdiffs())[0]?.added).toEqual(["optional:downloads"]);
4006
- await store.setlastpermissions(["required:storage"], "1.1.62");
4007
- expect(await store.getlastpermissions()).toEqual({ permissions: ["required:storage"], version: "1.1.62" });
4008
- });
4009
-
4010
- it("records safedefaults applications with their first seen origins and deferred command events", async () => {
4011
- const store = new sessionmemory(new fakeadapter());
4012
- await store.addsafedefaultapplication({ origin: "https://first.example", firstseenat: 1 });
4013
- await store.addsafedefaultapplication({ origin: "https://first.example", firstseenat: 2 });
4014
- expect(await store.getsafedefaultapplications()).toEqual([{ origin: "https://first.example", firstseenat: 1 }]);
4015
- await store.adddeferredevent({
4016
- id: "d1",
4017
- stepid: "step",
4018
- kind: "click",
4019
- origin: "https://example.com",
4020
- reason: "bucket full",
4021
- resetsat: 10,
4022
- at: 1,
4023
- });
4024
- expect((await store.getdeferredevents())[0]?.resetsat).toBe(10);
4025
- });
4026
-
4027
- it("serves the transparency data of the transparencypage in one read", async () => {
4028
- const store = new sessionmemory(new fakeadapter());
4029
- await store.addallowlistorigin({ origin: "https://example.com", profileid: "profile", grantedat: 1 });
4030
- await store.addconnectallow({ senderid: "sender", displayname: "Bridge", addedat: 1 });
4031
- await store.addsecret({
4032
- vaultid: "v1",
4033
- label: "Bank login",
4034
- scope: "https://bank.example",
4035
- profileid: "profile",
4036
- provenance: "user",
4037
- algorithm: "sha-256",
4038
- digest: "sha256:abc",
4039
- createdat: 1,
4040
- });
4041
- await store.addpermdiff({ fromversion: "1.1.61", toversion: "1.1.62", added: [], removed: [], computedat: 1 });
4042
- await store.addsafedefaultapplication({ origin: "https://first.example", firstseenat: 1 });
4043
- await store.savegate({
4044
- gateid: "g1",
4045
- kind: "confirmpay",
4046
- stepid: "step",
4047
- runid: "run",
4048
- origin: "https://shop.example",
4049
- payload: {},
4050
- state: "open",
4051
- openedat: 1,
4052
- });
4053
- await store.adddeferredevent({
4054
- id: "d1",
4055
- stepid: "step",
4056
- kind: "click",
4057
- origin: "https://example.com",
4058
- reason: "bucket full",
4059
- resetsat: 10,
4060
- at: 1,
4061
- });
4062
- await store.addphishverdict({
4063
- origin: "https://pay.example.com",
4064
- distance: 0.2,
4065
- threshold: 0.4,
4066
- blocked: false,
4067
- reason: "under",
4068
- at: 1,
4069
- });
4070
- const view = await store.gettransparencyview();
4071
- expect(view.allowlist).toHaveLength(1);
4072
- expect(view.profiles).toEqual([]);
4073
- expect(view.connectallow).toHaveLength(1);
4074
- expect(view.permdiffs).toHaveLength(1);
4075
- expect(view.safedefaults).toHaveLength(1);
4076
- expect(view.vault[0]?.label).toBe("Bank login");
4077
- expect(view.gates[0]?.gateid).toBe("g1");
4078
- expect(view.resolutions).toEqual([]);
4079
- expect(view.deferred).toHaveLength(1);
4080
- expect(view.phishverdicts).toHaveLength(1);
4081
- });
4082
- });
4083
-
4084
- /* ── The 1.1.95 purge and export completeness of the stored families. ── */
4085
-
4086
- describe("sessionmemory purge and export completeness", () => {
4087
- it("lists the stored keys of every run trace and session trail the families wrote", async () => {
4088
- const store = new sessionmemory(new fakeadapter());
4089
- await store.setrunrecord({
4090
- runid: "run-one",
4091
- planid: "plan",
4092
- sessionid: "session",
4093
- state: "completed",
4094
- createdat: 1,
4095
- updatedat: 2,
4096
- });
4097
- await store.setrunrecord({
4098
- runid: "run-two",
4099
- planid: "plan",
4100
- sessionid: "session",
4101
- state: "running",
4102
- createdat: 3,
4103
- updatedat: 4,
4104
- });
4105
- await store.addsessionrecord({
4106
- id: "session",
4107
- name: "workspace",
4108
- createdat: 1,
4109
- tabs: [],
4110
- captures: [],
4111
- storage: [],
4112
- cookies: [],
4113
- tags: [],
4114
- });
4115
- const keys = await store.storedkeys();
4116
- expect(keys).toContain("runs");
4117
- expect(keys).toContain("runlogrun-one");
4118
- expect(keys).toContain("runscopesrun-one");
4119
- expect(keys).toContain("runtimeline:run-one");
4120
- expect(keys).toContain("provlog:run-one");
4121
- expect(keys).toContain("runlogrun-two");
4122
- expect(keys).toContain("trailsession");
4123
- expect(keys).toContain("audit");
4124
- });
4125
-
4126
- it("purges every trace key of one run while the other runs and the audit family survive", async () => {
4127
- const store = new sessionmemory(new fakeadapter());
4128
- await store.setrunrecord({
4129
- runid: "run-one",
4130
- planid: "plan",
4131
- sessionid: "session",
4132
- state: "completed",
4133
- createdat: 1,
4134
- updatedat: 2,
4135
- });
4136
- await store.setrunrecord({
4137
- runid: "run-two",
4138
- planid: "plan",
4139
- sessionid: "session",
4140
- state: "running",
4141
- createdat: 3,
4142
- updatedat: 4,
4143
- });
4144
- await store.setrunscopes("run-one", []);
4145
- await store.setrunscopes("run-two", []);
4146
- const keys = await store.storedkeys();
4147
- const trace = keys.filter((key) => key.includes("run-one"));
4148
- expect(trace.length).toBeGreaterThan(0);
4149
- await store.purgekeys(trace);
4150
- expect(await store.getrunscopes("run-one")).toEqual([]);
4151
- expect(keys.filter((key) => key.includes("run-two")).length).toBeGreaterThan(0);
4152
- expect(keys).toContain("runs");
4153
- expect(keys).toContain("audit");
4154
- });
4155
- });