@wenathlan/devthink 2.0.12 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
@@ -0,0 +1,137 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ datagridcolumnsof,
4
+ datagridof,
5
+ exportdatagrid,
6
+ exportmenudescriptors,
7
+ exportrowsof,
8
+ filterdatagridrows,
9
+ infercolumntype,
10
+ maskedvalueof,
11
+ selectrowrange,
12
+ sortdatagridrows,
13
+ } from "../views.js";
14
+
15
+ const now = 1_800_000_000_000;
16
+
17
+ describe("datagrid and exportmenu", () => {
18
+ it("infers the column types from the observed values", () => {
19
+ expect(infercolumntype(["1", "2.5", "-3"])).toBe("number");
20
+ expect(infercolumntype(["true", "false"])).toBe("boolean");
21
+ expect(infercolumntype(["2024-05-01", "2024-06-15"])).toBe("date");
22
+ expect(infercolumntype(["alpha", "beta"])).toBe("text");
23
+ expect(infercolumntype(["", ""])).toBe("empty");
24
+ const columns = datagridcolumnsof([{ name: "alpha", price: "10", live: "true" }]);
25
+ expect(columns.find((column) => column.field === "name")?.type).toBe("text");
26
+ expect(columns.find((column) => column.field === "price")?.type).toBe("number");
27
+ expect(columns.find((column) => column.field === "live")?.type).toBe("boolean");
28
+ expect(columns.every((column) => column.inferred)).toBe(true);
29
+ });
30
+
31
+ it("builds the datagrid view of an extraction result and refuses empty shapes", () => {
32
+ const view = datagridof({
33
+ title: " Pricing table ",
34
+ origin: "https://example.com",
35
+ runid: "run-1",
36
+ rows: [
37
+ { item: "screwdriver", price: "12" },
38
+ { item: "hammer", price: "20" },
39
+ ],
40
+ at: now,
41
+ });
42
+ expect(view.title).toBe("Pricing table");
43
+ expect(view.rows).toHaveLength(2);
44
+ expect(view.rows[0]?.index).toBe(0);
45
+ expect(view.columns.map((column) => column.field)).toEqual(["item", "price"]);
46
+ expect(() =>
47
+ datagridof({ title: "", origin: "https://example.com", runid: "run-1", rows: [{ a: "b" }], at: now }),
48
+ ).toThrow(/title/);
49
+ expect(() => datagridof({ title: "grid", origin: " ", runid: "run-1", rows: [{ a: "b" }], at: now })).toThrow(
50
+ /origin/,
51
+ );
52
+ expect(() =>
53
+ datagridof({ title: "grid", origin: "https://example.com", runid: "run-1", rows: [], at: now }),
54
+ ).toThrow(/row/);
55
+ });
56
+
57
+ it("sorts and filters rows locally inside the surface", () => {
58
+ const view = datagridof({
59
+ title: "grid",
60
+ origin: "https://example.com",
61
+ runid: "run-1",
62
+ rows: [
63
+ { item: "hammer", price: "20" },
64
+ { item: "screwdriver", price: "12" },
65
+ { item: "wrench", price: "15" },
66
+ ],
67
+ at: now,
68
+ });
69
+ const sorted = sortdatagridrows(view, { field: "price", direction: "ascending" });
70
+ expect(sorted.rows.map((row) => row.values.item)).toEqual(["screwdriver", "wrench", "hammer"]);
71
+ const descending = sortdatagridrows(view, { field: "price", direction: "descending" });
72
+ expect(descending.rows.map((row) => row.values.item)).toEqual(["hammer", "wrench", "screwdriver"]);
73
+ expect(() => sortdatagridrows(view, { field: "unknown", direction: "ascending" })).toThrow(/column/);
74
+ const filtered = filterdatagridrows(view, "HAMMER");
75
+ expect(filtered.rows.map((row) => row.values.item)).toEqual(["hammer"]);
76
+ expect(filterdatagridrows(view, "").rows).toHaveLength(3);
77
+ });
78
+
79
+ it("selects a row range for a partial export and refuses a range outside the grid", () => {
80
+ const view = datagridof({
81
+ title: "grid",
82
+ origin: "https://example.com",
83
+ runid: "run-1",
84
+ rows: [{ a: "1" }, { a: "2" }, { a: "3" }],
85
+ at: now,
86
+ });
87
+ const selected = selectrowrange(view, 1, 2);
88
+ expect(selected.rows.map((row) => row.selected)).toEqual([false, true, true]);
89
+ expect(exportrowsof(selected, "selection")).toHaveLength(2);
90
+ expect(exportrowsof(selected, "run")).toHaveLength(3);
91
+ expect(() => selectrowrange(view, 2, 1)).toThrow(/range/);
92
+ expect(() => exportrowsof(view, "selection")).toThrow(/selected row range/);
93
+ });
94
+
95
+ it("offers csv, json and clipboard formats scoped to the selection, the step and the run", () => {
96
+ const menu = exportmenudescriptors();
97
+ expect(menu.filter((descriptor) => descriptor.format === "csv")).toHaveLength(3);
98
+ expect(menu.map((descriptor) => descriptor.scope)).toContain("selection");
99
+ expect(menu.map((descriptor) => descriptor.scope)).toContain("step");
100
+ expect(menu.map((descriptor) => descriptor.scope)).toContain("run");
101
+ expect(
102
+ menu
103
+ .filter((descriptor) => descriptor.format === "clipboard")
104
+ .every((descriptor) => descriptor.destination === "clipboard"),
105
+ ).toBe(true);
106
+ });
107
+
108
+ it("writes masked values only, honoring the maskinputs verdicts", () => {
109
+ const view = datagridof({
110
+ title: "grid",
111
+ origin: "https://example.com",
112
+ runid: "run-1",
113
+ rows: [{ user: "ada", token: "s3cr3tlongvalue" }],
114
+ at: now,
115
+ });
116
+ const maskverdicts = { token: "The token value stays masked and never renders in the clear." };
117
+ const masked = maskedvalueof("s3cr3tlongvalue", "token", maskverdicts);
118
+ expect(masked.masked).toBe(true);
119
+ expect(masked.value).not.toContain("s3cr3t");
120
+ expect(maskedvalueof("ada", "user", maskverdicts).masked).toBe(false);
121
+ const csv = exportdatagrid(view, { format: "csv", scope: "run", destination: "download" }, maskverdicts);
122
+ expect(csv.maskedfields).toEqual(["token"]);
123
+ expect(csv.text).not.toContain("s3cr3tlongvalue");
124
+ expect(csv.text).toContain("ada");
125
+ expect(csv.rows).toBe(1);
126
+ const json = exportdatagrid(view, { format: "json", scope: "run", destination: "memory" }, maskverdicts);
127
+ const parsed = JSON.parse(json.text) as {
128
+ rows: Array<Record<string, unknown>>;
129
+ columns: Array<{ field: string; type: string }>;
130
+ };
131
+ expect(parsed.columns.find((column) => column.field === "user")?.type).toBe("text");
132
+ expect(String(parsed.rows[0]?.token)).not.toContain("s3cr3t");
133
+ const clipboard = exportdatagrid(view, { format: "clipboard", scope: "run", destination: "clipboard" }, {});
134
+ expect(clipboard.text).toContain("s3cr3tlongvalue");
135
+ expect(clipboard.maskedfields).toEqual([]);
136
+ });
137
+ });
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { execFile } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { readFile } from "node:fs/promises";
5
+ import { promisify } from "node:util";
6
+
7
+ const execute = promisify(execFile);
8
+
9
+ /* ── The documentation consistency check coverage. ── */
10
+
11
+ describe("the documentation consistency artifact", () => {
12
+ it("declares every check family the gate runs over the documentation", async () => {
13
+ const source = await readFile("tests/doccheck.mjs", "utf8");
14
+ for (const family of [
15
+ "kindcoverage",
16
+ "kindfields",
17
+ "kindexamples",
18
+ "doclinks",
19
+ "codefences",
20
+ "referencetables",
21
+ "flowdiagrams",
22
+ "changelogcoverage",
23
+ "roadmapcoverage",
24
+ "readmeclaims",
25
+ ])
26
+ expect(source).toContain(`record("${family}"`);
27
+ /* the gate reads the compiled catalogs and joins the validate chain after the build */
28
+ expect(source).toContain("dist/policy.js");
29
+ expect(source).toContain("dist/index.js");
30
+ expect(source).toContain("dist/protocol.js");
31
+ expect(source).toContain("process.exitCode = 1");
32
+ });
33
+
34
+ it("covers every check family with a passing outcome once the gate has run", async () => {
35
+ const artifactpath = "tests/artifacts/doccheck.json";
36
+ const built = existsSync("dist/policy.js");
37
+ const packagejson = JSON.parse(await readFile("package.json", "utf8")) as { version: string };
38
+ /* a built tree runs the gate itself when no fresh artifact of this release exists, so every lane that builds before the suite verifies the documentation artifact without depending on gate ordering */
39
+ if (built && !existsSync(artifactpath)) await execute("node", ["tests/doccheck.mjs"]);
40
+ if (built && existsSync(artifactpath)) {
41
+ const stored = JSON.parse(await readFile(artifactpath, "utf8")) as { release: string };
42
+ if (stored.release !== packagejson.version) await execute("node", ["tests/doccheck.mjs"]);
43
+ }
44
+ const stored = existsSync(artifactpath);
45
+ if (built) expect(stored).toBe(true);
46
+ if (!stored) return;
47
+ const report = JSON.parse(await readFile(artifactpath, "utf8")) as {
48
+ release: string;
49
+ kindcount: number;
50
+ checks: Array<{ name: string; ok: boolean }>;
51
+ summary: { total: number; passed: number; failed: number };
52
+ };
53
+ expect(report.release).toBe(packagejson.version);
54
+ expect(report.summary.failed).toBe(0);
55
+ expect(report.summary.passed).toBe(report.summary.total);
56
+ expect(report.kindcount).toBeGreaterThan(300);
57
+ expect(report.checks.map((check) => check.name)).toEqual([
58
+ "kindcoverage",
59
+ "kindfields",
60
+ "kindexamples",
61
+ "doclinks",
62
+ "codefences",
63
+ "referencetables",
64
+ "flowdiagrams",
65
+ "changelogcoverage",
66
+ "roadmapcoverage",
67
+ "readmeclaims",
68
+ ]);
69
+ });
70
+
71
+ it("keeps the kind documentation generated from the module surface and the flows mermaid", async () => {
72
+ const kinddocs = await readFile("docs/kinddocs.md", "utf8");
73
+ const headings = (kinddocs.match(/^### `([a-z0-9]+)`$/gm) ?? []).length;
74
+ expect(headings).toBeGreaterThan(300);
75
+ for (const family of [
76
+ "## Interaction kinds",
77
+ "## Observation kinds",
78
+ "## Navigation kinds",
79
+ "## Tab and window kinds",
80
+ "## Form and data kinds",
81
+ "## Capture kinds",
82
+ "## Network kinds",
83
+ "## Debugging kinds",
84
+ "## Memory kinds",
85
+ "## Workflow kinds",
86
+ "## Protocol kinds",
87
+ "## Coordination kinds",
88
+ ])
89
+ expect(kinddocs).toContain(family);
90
+ const flowdocs = await readFile("docs/flowdocs.md", "utf8");
91
+ expect((flowdocs.match(/```mermaid/g) ?? []).length).toBeGreaterThanOrEqual(12);
92
+ const refdocs = await readFile("docs/refdocs.md", "utf8");
93
+ for (const table of [
94
+ "## cli command reference",
95
+ "## protocol message reference",
96
+ "## error code reference",
97
+ "## audit event reference",
98
+ "## configuration key reference",
99
+ "## storage schema reference",
100
+ "## capability manifest reference",
101
+ "## mcp tool reference",
102
+ "## trigger and scheduler reference",
103
+ "## permission and consent reference",
104
+ ])
105
+ expect(refdocs).toContain(table);
106
+ });
107
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ batchqueryplanof,
4
+ batchquerysavings,
5
+ batchquerytaskof,
6
+ coalescedombursts,
7
+ debounceprofilesof,
8
+ mutationbatchof,
9
+ runbatchquery,
10
+ } from "../perf.js";
11
+ import { batchqueryplangate, debouncewindowvalid } from "../policy.js";
12
+
13
+ const now = 1_800_000_000_000;
14
+
15
+ describe("debouncedom coalescing across event storms", () => {
16
+ it("builds the debounce profiles of the user configured windows only", () => {
17
+ const profiles = debounceprofilesof({ scroll: 100, input: 50 });
18
+ expect(profiles).toEqual([
19
+ { kind: "scroll", window: 100 },
20
+ { kind: "input", window: 50 },
21
+ ]);
22
+ expect(debounceprofilesof({})).toEqual([]);
23
+ });
24
+
25
+ it("coalesces scroll, input and resize storms per window and closes the batch when the window elapses", () => {
26
+ const profiles = [
27
+ { kind: "scroll" as const, window: 100 },
28
+ { kind: "input" as const, window: 200 },
29
+ ];
30
+ const events = [
31
+ { kind: "scroll" as const, at: now },
32
+ { kind: "scroll" as const, at: now + 40 },
33
+ { kind: "scroll" as const, at: now + 80 },
34
+ { kind: "input" as const, at: now },
35
+ { kind: "input" as const, at: now + 250 },
36
+ ];
37
+ const batches = coalescedombursts({ events, profiles, now: now + 90 });
38
+ const scroll = batches.find((batch) => batch.kind === "scroll");
39
+ expect(scroll?.count).toBe(3);
40
+ expect(scroll?.closed).toBe(false);
41
+ const inputbatches = batches.filter((batch) => batch.kind === "input");
42
+ expect(inputbatches).toHaveLength(2);
43
+ expect(inputbatches[0]?.count).toBe(1);
44
+ expect(inputbatches[0]?.closed).toBe(true);
45
+ const closed = coalescedombursts({ events, profiles, now: now + 500 }).filter((batch) => batch.kind === "scroll");
46
+ expect(closed[0]?.closed).toBe(true);
47
+ });
48
+
49
+ it("debounces one rapid mutation burst of the dom observer into a single batch", () => {
50
+ const burst = mutationbatchof({
51
+ mutations: [{ at: now }, { at: now + 5 }, { at: now + 9 }],
52
+ window: 100,
53
+ now: now + 10,
54
+ });
55
+ expect(burst.kind).toBe("mutation");
56
+ expect(burst.count).toBe(3);
57
+ expect(burst.closed).toBe(false);
58
+ const spread = mutationbatchof({ mutations: [{ at: now }, { at: now + 500 }], window: 100, now: now + 600 });
59
+ expect(spread.closed).toBe(true);
60
+ const unbounded = mutationbatchof({ mutations: [{ at: now }, { at: now + 9999 }], now: now + 10000 });
61
+ expect(unbounded.closed).toBe(true);
62
+ expect(unbounded.count).toBe(2);
63
+ });
64
+
65
+ it("validates the windows as user choices with no engine default", () => {
66
+ expect(debouncewindowvalid({ kind: "scroll", window: 100 }).allowed).toBe(true);
67
+ expect(debouncewindowvalid({ kind: "scroll" }).allowed).toBe(true);
68
+ expect(debouncewindowvalid({ kind: "scroll" }).reason).toMatch(/pass through uncoalesced/i);
69
+ expect(debouncewindowvalid({ kind: "input", window: 0 }).allowed).toBe(false);
70
+ expect(debouncewindowvalid({ kind: "input", window: -5 }).allowed).toBe(false);
71
+ });
72
+ });
73
+
74
+ describe("batchquery folding of repeated selectors", () => {
75
+ it("folds repeated selectors into one pass of distinct selectors", () => {
76
+ const plan = batchqueryplanof(["#a", "#b", "#a", "#c", "#b", "#a"]);
77
+ expect(plan.selectors).toEqual(["#a", "#b", "#c"]);
78
+ expect(plan.folded).toBe(3);
79
+ expect(plan.onepass).toBe(true);
80
+ expect(batchqueryplangate(plan).allowed).toBe(true);
81
+ expect(batchqueryplangate({ selectors: ["#a", "#a"], folded: 0, onepass: true }).allowed).toBe(false);
82
+ expect(batchqueryplangate({ selectors: ["#a"], folded: 0, onepass: false }).allowed).toBe(false);
83
+ expect(batchqueryplangate({ selectors: [], folded: 0, onepass: true }).allowed).toBe(false);
84
+ });
85
+
86
+ it("executes the grouped selectors in one pass with one resolver call per distinct selector", () => {
87
+ const calls: string[] = [];
88
+ const pass = runbatchquery({
89
+ plan: batchqueryplanof(["#a", "#b", "#a"]),
90
+ resolver: (selector) => {
91
+ calls.push(selector);
92
+ return `resolved:${selector}`;
93
+ },
94
+ });
95
+ expect(calls).toEqual(["#a", "#b"]);
96
+ expect(pass.queries).toBe(2);
97
+ expect(pass.resolutions).toEqual({ "#a": "resolved:#a", "#b": "resolved:#b" });
98
+ const savings = batchquerysavings(batchqueryplanof(["#a", "#a", "#a"]));
99
+ expect(savings).toEqual({ distinct: 1, folded: 2, saved: 2 });
100
+ });
101
+
102
+ it("shapes the plan into one offscreen worker task payload", () => {
103
+ const task = batchquerytaskof({ plan: batchqueryplanof(["#a", "#b", "#a"]), runid: "run1", stepid: "s1" });
104
+ expect(task.task).toBe("batchquery");
105
+ expect(task.selectors).toEqual(["#a", "#b"]);
106
+ expect(task.folded).toBe(1);
107
+ expect(task.runid).toBe("run1");
108
+ });
109
+ });
@@ -0,0 +1,282 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ agentgrammarvalid,
4
+ agentpresetof,
5
+ applylayer,
6
+ blackboxmatches,
7
+ blackboxruleof,
8
+ blackboxedurls,
9
+ browserpermissions,
10
+ devicepresetof,
11
+ emulationkinds,
12
+ emulationstateof,
13
+ expirelayers,
14
+ exportpresetlibrary,
15
+ familyofkind,
16
+ hideblackboxedframes,
17
+ importpresetlibrary,
18
+ locationconsentcovers,
19
+ locationpresetof,
20
+ locationrangevalid,
21
+ layernames,
22
+ networkpresetof,
23
+ newlayer,
24
+ permissiongrade,
25
+ permissiongrantof,
26
+ permissionstates,
27
+ revertalllayers,
28
+ revertlayer,
29
+ revertplanof,
30
+ stackedcount,
31
+ } from "../environments.js";
32
+ import type { emulationlayer, stackframe } from "../types.js";
33
+
34
+ const now = 1_800_000_000_000;
35
+
36
+ const device = { name: "phone", width: 390, height: 844, pixelratio: 3, mobile: true };
37
+ const network = { name: "slow3g", latency: 400, download: 400, upload: 400, offline: false };
38
+ const location = { name: "lisbon", latitude: 38.7223, longitude: -9.1393, accuracy: 100 };
39
+ const agent = {
40
+ name: "desktopmask",
41
+ useragent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
42
+ platform: "Linux x86_64",
43
+ brands: ["Chromium", "Not A;Brand"],
44
+ };
45
+
46
+ function layer(id: string, family: emulationlayer["family"], name: string, at: number): emulationlayer {
47
+ return newlayer({
48
+ id,
49
+ runid: "run",
50
+ stepid: `step-${id}`,
51
+ family,
52
+ name,
53
+ originscope: "https://example.com",
54
+ revertplan: ["revert the layer", "restore the prior page state"],
55
+ at,
56
+ });
57
+ }
58
+
59
+ describe("emulation presets", () => {
60
+ it("normalizes the device, network, location and agent presets of the user curated libraries", () => {
61
+ expect(devicepresetof(device)).toEqual(device);
62
+ expect(devicepresetof({ name: "", width: 390, height: 844, pixelratio: 3 })).toBeUndefined();
63
+ expect(devicepresetof({ name: "x", width: 0, height: 844, pixelratio: 3 })).toBeUndefined();
64
+ expect(devicepresetof({ name: "x", width: 390, height: 844, pixelratio: -1 })).toBeUndefined();
65
+ expect(devicepresetof({ name: "x", width: 1.5, height: 844, pixelratio: 3 })).toBeUndefined();
66
+ expect(networkpresetof(network)).toEqual(network);
67
+ expect(networkpresetof({ name: "x", latency: -1, download: 1, upload: 1 })).toBeUndefined();
68
+ expect(networkpresetof({ name: "x", latency: 0, download: 0, upload: 0, offline: true })?.offline).toBe(true);
69
+ expect(locationpresetof(location)).toEqual(location);
70
+ expect(locationpresetof({ name: "x", latitude: 91, longitude: 0, accuracy: 5 })).toBeUndefined();
71
+ expect(agentpresetof(agent)).toEqual(agent);
72
+ expect(agentpresetof({ ...agent, brands: [] })).toBeUndefined();
73
+ expect(agentpresetof({ ...agent, platform: " " })).toBeUndefined();
74
+ });
75
+
76
+ it("maps every emulation kind onto its layer family", () => {
77
+ expect(familyofkind("emulatedevice")).toBe("device");
78
+ expect(familyofkind("emulatenetwork")).toBe("network");
79
+ expect(familyofkind("emulatelocate")).toBe("location");
80
+ expect(familyofkind("setuseragent")).toBe("agent");
81
+ expect(familyofkind("overridepermission")).toBe("permission");
82
+ expect(familyofkind("blackboxscripts")).toBe("blackbox");
83
+ expect(familyofkind("click")).toBeUndefined();
84
+ expect(emulationkinds).toHaveLength(6);
85
+ });
86
+ });
87
+
88
+ describe("emulation grammars", () => {
89
+ it("validates the latitude and longitude ranges of the location layer", () => {
90
+ expect(locationrangevalid(0, 0)).toBe(true);
91
+ expect(locationrangevalid(90, 180)).toBe(true);
92
+ expect(locationrangevalid(-90, -180)).toBe(true);
93
+ expect(locationrangevalid(90.1, 0)).toBe(false);
94
+ expect(locationrangevalid(0, -180.1)).toBe(false);
95
+ expect(locationrangevalid(Number.NaN, 0)).toBe(false);
96
+ });
97
+
98
+ it("validates the user agent string against the reviewed grammar", () => {
99
+ expect(agentgrammarvalid(agent.useragent)).toBe(true);
100
+ expect(agentgrammarvalid("curl/8.5.0")).toBe(true);
101
+ expect(agentgrammarvalid("no version token")).toBe(false);
102
+ expect(agentgrammarvalid("Mozilla/5.0\r\nX-Injected: 1")).toBe(false);
103
+ expect(agentgrammarvalid("bad<script>")).toBe(false);
104
+ expect(agentgrammarvalid("x".repeat(513))).toBe(false);
105
+ });
106
+
107
+ it("validates the permission overrides against the reviewed browser permission set and grades them by name", () => {
108
+ expect(permissiongrantof({ name: "geolocation", state: "granted", runscope: true })).toEqual({
109
+ name: "geolocation",
110
+ state: "granted",
111
+ runscope: true,
112
+ });
113
+ expect(permissiongrantof({ name: "camera", state: "denied" })).toEqual({
114
+ name: "camera",
115
+ state: "denied",
116
+ runscope: true,
117
+ });
118
+ expect(permissiongrantof({ name: "screen-capture", state: "granted" })).toBeUndefined();
119
+ expect(permissiongrantof({ name: "geolocation", state: "maybe" })).toBeUndefined();
120
+ expect(permissionstates).toEqual(["granted", "denied", "prompt"]);
121
+ expect(permissiongrade("geolocation")).toBe("powerful");
122
+ expect(permissiongrade("camera")).toBe("powerful");
123
+ expect(permissiongrade("microphone")).toBe("powerful");
124
+ expect(permissiongrade("notifications")).toBe("powerful");
125
+ expect(permissiongrade("clipboard-write")).toBe("standard");
126
+ expect(browserpermissions).toContain("geolocation");
127
+ });
128
+
129
+ it("normalizes the blackbox rules and matches third party urls with stars and subtrees", () => {
130
+ const rule = { urlpatterns: ["https://cdn.example/vendor/**"], tracescope: "both" as const };
131
+ expect(blackboxruleof(rule)).toEqual(rule);
132
+ expect(blackboxruleof({ urlpatterns: [], tracescope: "both" })).toBeUndefined();
133
+ expect(blackboxruleof({ urlpatterns: ["https://cdn.example/**"], tracescope: "everywhere" })).toBeUndefined();
134
+ expect(blackboxruleof({ urlpatterns: ["https://cdn.example/**"] })).toBeUndefined();
135
+ expect(blackboxmatches("https://cdn.example/vendor/**", "https://cdn.example/vendor/app.js")).toBe(true);
136
+ expect(blackboxmatches("https://cdn.example/vendor/**", "https://cdn.example/other.js")).toBe(false);
137
+ expect(blackboxmatches("https://cdn.example/*", "https://cdn.example/app.js")).toBe(true);
138
+ expect(blackboxmatches("https://cdn.example/*", "https://cdn.example/nested/app.js")).toBe(false);
139
+ expect(blackboxmatches("https://cdn.example/**", "https://elsewhere.example/app.js")).toBe(false);
140
+ });
141
+
142
+ it("hides blackboxed frames from stack traces and lists the blackboxed urls of a trace", () => {
143
+ const frames: stackframe[] = [
144
+ { url: "https://example.com/app.js", line: 1 },
145
+ { url: "https://cdn.example/vendor/lib.js", line: 2 },
146
+ { url: "https://example.com/main.js", line: 3 },
147
+ ];
148
+ const traces = [{ urlpatterns: ["https://cdn.example/**"], tracescope: "traces" as const }];
149
+ expect(hideblackboxedframes(traces, frames)).toEqual([frames[0], frames[2]]);
150
+ const profiles = [{ urlpatterns: ["https://cdn.example/**"], tracescope: "profiles" as const }];
151
+ expect(hideblackboxedframes(profiles, frames)).toHaveLength(3);
152
+ expect(
153
+ blackboxedurls(
154
+ [...traces, ...profiles],
155
+ frames.map((frame) => frame.url),
156
+ ),
157
+ ).toEqual(["https://cdn.example/vendor/lib.js"]);
158
+ expect(hideblackboxedframes([], frames)).toHaveLength(3);
159
+ });
160
+
161
+ it("requires a non-empty reviewed revert plan beside every layer", () => {
162
+ expect(revertplanof(["detach", "restore the prior state"])).toEqual(["detach", "restore the prior state"]);
163
+ expect(revertplanof([])).toBeUndefined();
164
+ expect(revertplanof(["", " "])).toBeUndefined();
165
+ expect(revertplanof("detach")).toBeUndefined();
166
+ });
167
+ });
168
+
169
+ describe("emulation layers", () => {
170
+ it("applies and reverts device metric layers with the prior state captured for the exact revert", () => {
171
+ const state = emulationstateof({ runid: "run", tabid: 4, origin: "https://example.com", now });
172
+ const withprior = newlayer({
173
+ id: "l1",
174
+ runid: "run",
175
+ stepid: "s1",
176
+ family: "device",
177
+ name: "phone",
178
+ originscope: "https://example.com",
179
+ revertplan: ["restore the pixel ratio"],
180
+ prior: { pixelratio: 2, viewportwidth: 1200, viewportheight: 800 },
181
+ at: now,
182
+ });
183
+ const applied = applylayer(state, withprior, now + 10);
184
+ expect(applied.layers).toHaveLength(1);
185
+ expect(applied.layers[0]?.prior?.pixelratio).toBe(2);
186
+ const reverted = revertlayer(applied, "l1", now + 20);
187
+ expect(reverted.layers[0]?.revertedat).toBe(now + 20);
188
+ expect(layernames(reverted)).toEqual([]);
189
+ });
190
+
191
+ it("stacks layers with the last applied winning conflicts and reverts them in reverse order", () => {
192
+ const base = emulationstateof({ runid: "run", tabid: 4, origin: "https://example.com", now });
193
+ let current = applylayer(base, layer("l1", "device", "phone", now), now + 1);
194
+ current = applylayer(current, layer("l2", "network", "slow3g", now), now + 2);
195
+ current = applylayer(current, layer("l3", "agent", "desktopmask", now), now + 3);
196
+ expect(stackedcount(current)).toBe(3);
197
+ expect(layernames(current)).toEqual(["phone", "slow3g", "desktopmask"]);
198
+ const second = applylayer(current, layer("l4", "device", "tablet", now + 5), now + 5);
199
+ expect(layernames(second)).toEqual(["phone", "slow3g", "desktopmask", "tablet"]);
200
+ const outcome = revertalllayers(second, now + 30);
201
+ expect(outcome.reverted.map((entry) => entry.name)).toEqual(["tablet", "desktopmask", "slow3g", "phone"]);
202
+ expect(layernames(outcome.state)).toEqual([]);
203
+ expect(stackedcount(outcome.state)).toBe(0);
204
+ });
205
+
206
+ it("keeps the layer history of reverted layers for review", () => {
207
+ const state = applylayer(
208
+ emulationstateof({ runid: "run", tabid: 4, origin: "https://example.com", now }),
209
+ layer("l1", "location", "lisbon", now),
210
+ now,
211
+ );
212
+ const reverted = revertalllayers(state, now + 10);
213
+ expect(reverted.state.layers).toHaveLength(1);
214
+ expect(reverted.state.layers[0]?.revertedat).toBe(now + 10);
215
+ });
216
+ });
217
+
218
+ describe("emulation retention and presets", () => {
219
+ it("expires the prior states of reverted layers after the retention window while the history survives", () => {
220
+ const state = revertalllayers(
221
+ applylayer(
222
+ emulationstateof({ runid: "run", tabid: 4, origin: "https://example.com", now }),
223
+ newlayer({
224
+ id: "l1",
225
+ runid: "run",
226
+ stepid: "s1",
227
+ family: "agent",
228
+ name: "desktopmask",
229
+ originscope: "https://example.com",
230
+ revertplan: ["restore the agent"],
231
+ prior: { useragent: "UA" },
232
+ at: now,
233
+ }),
234
+ now,
235
+ ),
236
+ now + 10,
237
+ ).state;
238
+ expect(expirelayers(state, 1000, now + 100).layers[0]?.prior).toEqual({ useragent: "UA" });
239
+ const expired = expirelayers(state, 1000, now + 2000);
240
+ expect(expired.layers[0]?.prior).toBeUndefined();
241
+ expect(expired.layers[0]?.priorexpired).toBe(true);
242
+ expect(expired.layers[0]?.name).toBe("desktopmask");
243
+ expect(expirelayers(state, undefined, now + 100_000).layers[0]?.prior).toEqual({ useragent: "UA" });
244
+ });
245
+
246
+ it("exports and imports the versioned preset library through review", () => {
247
+ const file = exportpresetlibrary({
248
+ devices: [device],
249
+ networks: [network],
250
+ locations: [location],
251
+ agents: [agent],
252
+ now,
253
+ });
254
+ expect(file.version).toBe(1);
255
+ expect(file.devices[0]?.name).toBe("phone");
256
+ const parsed = importpresetlibrary(JSON.parse(JSON.stringify(file)));
257
+ expect(parsed?.networks[0]?.name).toBe("slow3g");
258
+ expect(parsed?.locations[0]?.latitude).toBe(38.7223);
259
+ expect(importpresetlibrary({ devices: [{ name: "bad", width: 0, height: 0, pixelratio: 1 }] })).toBeUndefined();
260
+ expect(importpresetlibrary("nope")).toBeUndefined();
261
+ });
262
+
263
+ it("covers the location consent by origin and reviewed coordinates", () => {
264
+ const consents = [
265
+ {
266
+ id: "c1",
267
+ prompt: "Where?",
268
+ origin: "https://example.com",
269
+ latitude: 38.7223,
270
+ longitude: -9.1393,
271
+ approved: true,
272
+ consentedat: now,
273
+ },
274
+ ];
275
+ expect(locationconsentcovers("https://example.com", 38.7223, -9.1393, consents)).toBe(true);
276
+ expect(locationconsentcovers("https://example.com", 41.3851, 2.1734, consents)).toBe(false);
277
+ expect(locationconsentcovers("https://elsewhere.example", 38.7223, -9.1393, consents)).toBe(false);
278
+ expect(locationconsentcovers("https://example.com", 38.7223, -9.1393, [{ ...consents[0]!, approved: false }])).toBe(
279
+ false,
280
+ );
281
+ });
282
+ });