@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,259 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ closeoffscreen,
4
+ defaultenvironment,
5
+ environmentrequirementsof,
6
+ environmentsof,
7
+ executorregistry,
8
+ isolatedinjection,
9
+ markuprenderstep,
10
+ offloadkinds,
11
+ openoffscreen,
12
+ poolplan,
13
+ routeenvironment,
14
+ transferablekeys,
15
+ workerrequestof,
16
+ workerresponseof,
17
+ acceptworkerresponse,
18
+ } from "../environments.js";
19
+ import { reviewedkinds } from "../policy.js";
20
+ import type { toolstep } from "../types.js";
21
+
22
+ const now = 1_000;
23
+
24
+ function step(kind: toolstep["kind"], extra: Partial<toolstep> = {}): toolstep {
25
+ return { id: `${kind}-step`, kind, summary: `Run the ${kind} step.`, risk: "read", ...extra };
26
+ }
27
+
28
+ describe("execution environments", () => {
29
+ it("routes every reviewed action kind to an allowed environment set with a default", () => {
30
+ const kinds = reviewedkinds();
31
+ expect(kinds.length).toBeGreaterThan(330);
32
+ const requirements = environmentrequirementsof(kinds as toolstep["kind"][]);
33
+ expect(requirements).toHaveLength(kinds.length);
34
+ for (const requirement of requirements) {
35
+ expect(requirement.environments.length).toBeGreaterThan(0);
36
+ expect(requirement.environments).toContain(requirement.defaultenvironment);
37
+ for (const environment of requirement.environments)
38
+ expect(["pagecontext", "isolatedworld", "offscreenworker", "sandboxframe"]).toContain(environment);
39
+ }
40
+ });
41
+
42
+ it("marks evaluate steps as isolatedworld only and untrusted markup renders as sandboxframe only", () => {
43
+ expect(environmentsof(step("evaluate"))).toEqual(["isolatedworld"]);
44
+ expect(defaultenvironment(step("evaluate"))).toBe("isolatedworld");
45
+ const markupstep = step("setattribute", {
46
+ options: JSON.stringify({ name: "data-summary", value: "inert", markup: "<p>untrusted</p>" }),
47
+ });
48
+ expect(environmentsof(markupstep)).toEqual(["sandboxframe"]);
49
+ expect(defaultenvironment(markupstep)).toBe("sandboxframe");
50
+ expect(markuprenderstep(markupstep)).toBe(true);
51
+ expect(
52
+ markuprenderstep(step("setattribute", { options: JSON.stringify({ name: "data-summary", value: "inert" }) })),
53
+ ).toBe(false);
54
+ expect(markuprenderstep(step("setattribute", { options: "{not json" }))).toBe(false);
55
+ });
56
+
57
+ it("marks the parse heavy read kinds as offscreenworker eligible beside the live page", () => {
58
+ const families = offloadkinds();
59
+ expect(families.map((family) => family.task)).toEqual([
60
+ "htmlsnapshot",
61
+ "jsonpayload",
62
+ "tablerows",
63
+ "a11ytree",
64
+ "complexselector",
65
+ "stitchshots",
66
+ ]);
67
+ for (const kind of [
68
+ "readhtml",
69
+ "parsehtml",
70
+ "readjson",
71
+ "parsejson",
72
+ "readtable",
73
+ "scrapetable",
74
+ "a11ytree",
75
+ "resolvexpath",
76
+ "deriveselector",
77
+ "contactsheet",
78
+ "timelapse",
79
+ "makethumbs",
80
+ ] as const) {
81
+ expect(environmentsof(step(kind))).toEqual(["pagecontext", "offscreenworker"]);
82
+ expect(defaultenvironment(step(kind))).toBe("pagecontext");
83
+ }
84
+ expect(environmentsof(step("click"))).toEqual(["pagecontext"]);
85
+ expect(environmentsof(step("fillform"))).toEqual(["pagecontext"]);
86
+ });
87
+
88
+ it("maps every environmentkind to its runtime adapter in the executor registry", () => {
89
+ const registry = executorregistry();
90
+ expect(registry.map((entry) => entry.environment)).toEqual([
91
+ "pagecontext",
92
+ "isolatedworld",
93
+ "offscreenworker",
94
+ "sandboxframe",
95
+ ]);
96
+ expect(registry.map((entry) => entry.adapter)).toEqual([
97
+ "pagebridge",
98
+ "scriptingapi",
99
+ "offscreendocument",
100
+ "sandboxpage",
101
+ ]);
102
+ for (const entry of registry) expect(entry.description).toMatch(/\./);
103
+ });
104
+
105
+ it("routes steps by the reviewed environment field, the offload toggle and the capability grant with an inline fallback", () => {
106
+ expect(routeenvironment(step("readhtml"), { offload: false, granted: true }).environment).toBe("pagecontext");
107
+ expect(routeenvironment(step("readhtml"), { offload: true, granted: true })).toMatchObject({
108
+ environment: "offscreenworker",
109
+ fallback: false,
110
+ });
111
+ const fallback = routeenvironment(step("readhtml"), { offload: true, granted: false });
112
+ expect(fallback).toMatchObject({ environment: "pagecontext", fallback: true });
113
+ expect(fallback.reason).toMatch(/inline parsing/i);
114
+ expect(routeenvironment(step("evaluate"), { offload: true, granted: true }).environment).toBe("isolatedworld");
115
+ expect(routeenvironment(step("click"), { offload: true, granted: true }).environment).toBe("pagecontext");
116
+ const named = routeenvironment(step("readhtml", { environment: "offscreenworker" }), {
117
+ offload: false,
118
+ granted: true,
119
+ });
120
+ expect(named.environment).toBe("offscreenworker");
121
+ const refused = routeenvironment(step("click", { environment: "isolatedworld" }), { offload: true, granted: true });
122
+ expect(refused.environment).toBe("pagecontext");
123
+ expect(refused.reason).toMatch(/outside the pagecontext the click kind permits/i);
124
+ });
125
+
126
+ it("builds worker requests with the parse family and the transferable keys and reads their answers", () => {
127
+ const buffer = new ArrayBuffer(8);
128
+ const request = workerrequestof({
129
+ id: "w1",
130
+ runid: "run1",
131
+ stepid: "s1",
132
+ kind: "readhtml",
133
+ payload: "<html><body>heavy</body></html>",
134
+ options: { snapshot: buffer, label: "front page" },
135
+ sentat: now,
136
+ });
137
+ expect(request.task).toBe("htmlsnapshot");
138
+ expect(request.transferables).toEqual(["snapshot"]);
139
+ const partial = acceptworkerresponse(
140
+ workerresponseof({
141
+ id: "a1",
142
+ requestid: request.id,
143
+ ok: true,
144
+ partial: 1,
145
+ summary: "The first chunk streams back.",
146
+ receivedat: now + 5,
147
+ }),
148
+ );
149
+ expect(partial).toMatchObject({ done: false, partial: true });
150
+ const final = acceptworkerresponse(
151
+ workerresponseof({
152
+ id: "a2",
153
+ requestid: request.id,
154
+ ok: true,
155
+ result: "front page text",
156
+ summary: "The parse finished.",
157
+ receivedat: now + 20,
158
+ }),
159
+ );
160
+ expect(final).toMatchObject({ done: true, partial: false, result: "front page text" });
161
+ const refused = acceptworkerresponse(
162
+ workerresponseof({
163
+ id: "a3",
164
+ requestid: request.id,
165
+ ok: false,
166
+ summary: "The payload was malformed.",
167
+ receivedat: now + 25,
168
+ }),
169
+ );
170
+ expect(refused.done).toBe(true);
171
+ expect(refused.result).toBeUndefined();
172
+ expect(transferablekeys({ snapshot: buffer, label: "text" })).toEqual(["snapshot"]);
173
+ expect(transferablekeys({ label: "text" })).toEqual([]);
174
+ expect(() =>
175
+ workerrequestof({ id: "w2", runid: "run1", stepid: "s2", kind: "click", payload: "x", sentat: now }),
176
+ ).toThrow(/outside the offscreen worker pool families/i);
177
+ expect(() =>
178
+ workerrequestof({ id: "w3", runid: "run1", stepid: "s3", kind: "readjson", payload: " ", sentat: now }),
179
+ ).toThrow(/payload/i);
180
+ expect(() => workerresponseof({ id: "w4", requestid: "r", ok: true, summary: " ", receivedat: now })).toThrow(
181
+ /summary/i,
182
+ );
183
+ });
184
+
185
+ it("grows and shrinks the worker pool with the pending parse queue and the user configured size with no engine cap", () => {
186
+ expect(poolplan({ pending: 5, current: 2 })).toMatchObject({ workers: 5, added: 3, retired: 0 });
187
+ expect(poolplan({ pending: 1, current: 4 })).toMatchObject({ workers: 1, added: 0, retired: 3 });
188
+ expect(poolplan({ pending: 3, current: 3 })).toMatchObject({ workers: 3, added: 0, retired: 0 });
189
+ expect(poolplan({ pending: 2, current: 1, size: 8 })).toMatchObject({ workers: 8, added: 7 });
190
+ expect(poolplan({ pending: 9, current: 6, size: 2 })).toMatchObject({ workers: 2, retired: 4 });
191
+ expect(poolplan({ pending: 2, current: 2, size: 2 }).reason).toMatch(/user configured/i);
192
+ expect(poolplan({ pending: 2, current: 2 }).reason).toMatch(/no engine cap|match/i);
193
+ expect(poolplan({ pending: 1, current: 1, size: 0 })).toMatchObject({ workers: 1, added: 0, retired: 0 });
194
+ });
195
+
196
+ it("spawns the offscreen document on first use, reuses it across the steps of one run and closes it at completion", () => {
197
+ const first = openoffscreen({
198
+ registry: [],
199
+ document: "offscreen.html",
200
+ runid: "run1",
201
+ reasons: ["DOM_PARSER", "WORKERS"],
202
+ justification: "Heavy parsing of reviewed snapshots.",
203
+ now,
204
+ });
205
+ expect(first.reused).toBe(false);
206
+ const second = openoffscreen({
207
+ registry: first.registry,
208
+ document: "offscreen.html",
209
+ runid: "run1",
210
+ reasons: ["DOM_PARSER"],
211
+ justification: "Heavy parsing of reviewed snapshots.",
212
+ now: now + 10,
213
+ });
214
+ expect(second.reused).toBe(true);
215
+ expect(second.entry).toBe(first.entry);
216
+ const otherrun = openoffscreen({
217
+ registry: first.registry,
218
+ document: "offscreen.html",
219
+ runid: "run2",
220
+ reasons: ["DOM_PARSER"],
221
+ justification: "Heavy parsing of reviewed snapshots.",
222
+ now: now + 20,
223
+ });
224
+ expect(otherrun.reused).toBe(false);
225
+ const closed = closeoffscreen(otherrun.registry, "run1", now + 30);
226
+ expect(closed.closed).toBe(true);
227
+ expect(closed.registry.find((entry) => entry.runid === "run1")?.closedat).toBe(now + 30);
228
+ expect(closeoffscreen(closed.registry, "run1", now + 40).closed).toBe(false);
229
+ expect(() =>
230
+ openoffscreen({ registry: [], document: " ", runid: "run1", reasons: ["DOM_PARSER"], justification: "j", now }),
231
+ ).toThrow(/document/i);
232
+ expect(() =>
233
+ openoffscreen({ registry: [], document: "offscreen.html", runid: "run1", reasons: [], justification: "j", now }),
234
+ ).toThrow(/reasons/i);
235
+ expect(() =>
236
+ openoffscreen({
237
+ registry: [],
238
+ document: "offscreen.html",
239
+ runid: "run1",
240
+ reasons: ["DOM_PARSER"],
241
+ justification: " ",
242
+ now,
243
+ }),
244
+ ).toThrow(/justification/i);
245
+ });
246
+
247
+ it("builds the isolated world injection of evaluate steps with reviewed arguments only", () => {
248
+ const injection = isolatedinjection(
249
+ step("evaluate", { value: "document.title", options: JSON.stringify(["arg one", "arg two"]) }),
250
+ );
251
+ expect(injection.world).toBe("ISOLATED");
252
+ expect(injection.code).toBe("document.title");
253
+ expect(injection.args).toEqual(["arg one", "arg two"]);
254
+ const bare = isolatedinjection(step("evaluate", { value: "1 + 1" }));
255
+ expect(bare.args).toEqual([]);
256
+ expect(() => isolatedinjection(step("evaluate", { value: " " }))).toThrow(/reviewed expression/i);
257
+ expect(() => isolatedinjection(step("click"))).toThrow(/evaluate kind only/i);
258
+ });
259
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ comparepairof,
4
+ comparepairsforsteps,
5
+ overlayslider,
6
+ shotpanelof,
7
+ shotpanelpan,
8
+ shotpanelzoom,
9
+ } from "../views.js";
10
+ import { shotpanelgate } from "../policy.js";
11
+
12
+ const now = 1_800_000_000_000;
13
+
14
+ describe("shotpanel and compareviewer", () => {
15
+ it("opens the shotpanel view of a granted origin with its redaction verdicts and refuses ungranted captures", () => {
16
+ const view = shotpanelof({
17
+ stepid: "step-2",
18
+ runid: "run-1",
19
+ captureid: "shot-9",
20
+ provenance: "fullpage",
21
+ origin: "https://example.com",
22
+ granted: ["https://example.com"],
23
+ redactions: [{ region: "top right card", verdict: "The card number region stays redacted." }],
24
+ at: now,
25
+ });
26
+ expect(view.captureid).toBe("shot-9");
27
+ expect(view.provenance).toBe("fullpage");
28
+ expect(view.redactions[0]?.verdict).toMatch(/redacted/);
29
+ expect(view.zoom).toBe(1);
30
+ expect(() =>
31
+ shotpanelof({
32
+ stepid: "step-2",
33
+ runid: "run-1",
34
+ captureid: "shot-9",
35
+ provenance: "viewport",
36
+ origin: "https://other.example",
37
+ granted: ["https://example.com"],
38
+ at: now,
39
+ }),
40
+ ).toThrow(/allowlist/);
41
+ expect(shotpanelgate({ captureorigin: "https://example.com", granted: ["https://example.com"] }).allowed).toBe(
42
+ true,
43
+ );
44
+ expect(shotpanelgate({ captureorigin: "https://other.example", granted: ["https://example.com"] }).reason).toMatch(
45
+ /granted origins/,
46
+ );
47
+ });
48
+
49
+ it("zooms and pans the large stitched captures", () => {
50
+ let view = shotpanelof({
51
+ stepid: "step-2",
52
+ runid: "run-1",
53
+ captureid: "shot-9",
54
+ provenance: "fullpage",
55
+ origin: "https://example.com",
56
+ granted: ["https://example.com"],
57
+ at: now,
58
+ });
59
+ view = shotpanelzoom(view, 2);
60
+ expect(view.zoom).toBe(2);
61
+ view = shotpanelzoom(view, 1.5);
62
+ expect(view.zoom).toBe(3);
63
+ view = shotpanelpan(view, { x: 40, y: -10 });
64
+ expect(view.pan).toEqual({ x: 40, y: -10 });
65
+ expect(() => shotpanelzoom(view, 0)).toThrow(/positive/);
66
+ });
67
+
68
+ it("pairs the before and after captures of every executed write step and overlays them with a slider", () => {
69
+ const pair = comparepairof({ stepid: "step-5", beforecaptureid: "shot-10", aftercaptureid: "shot-11" });
70
+ expect(pair.slidervalue).toBe(50);
71
+ const slid = overlayslider(pair, 80);
72
+ expect(slid.slidervalue).toBe(80);
73
+ expect(() => overlayslider(pair, 120)).toThrow(/between zero and one hundred/);
74
+ expect(() => comparepairof({ stepid: "step-5", beforecaptureid: "shot-10", aftercaptureid: "shot-10" })).toThrow(
75
+ /distinct/,
76
+ );
77
+ const pairs = comparepairsforsteps(
78
+ [
79
+ { stepid: "read-1", risk: "read", writeexecuted: false },
80
+ { stepid: "fill-2", risk: "sensitive", writeexecuted: true },
81
+ { stepid: "fill-3", risk: "sensitive", writeexecuted: false },
82
+ ],
83
+ { "fill-2": { beforecaptureid: "b-2", aftercaptureid: "a-2" }, "fill-3": { beforecaptureid: "b-3" } },
84
+ );
85
+ expect(pairs.map((candidate) => candidate.stepid)).toEqual(["fill-2"]);
86
+ expect(pairs[0]?.beforecaptureid).toBe("b-2");
87
+ });
88
+ });
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ chainrows,
4
+ csvfield,
5
+ csvof,
6
+ exportchain,
7
+ exportdescriptorof,
8
+ exportextractions,
9
+ exportnotes,
10
+ exportrecords,
11
+ noterow,
12
+ unmaskedfieldsof,
13
+ } from "../export.js";
14
+ import { defaultmaskshapes } from "../security.js";
15
+ import { appendlogentry, openrunlog } from "../security.js";
16
+
17
+ const now = 1_800_000_000_000;
18
+
19
+ describe("exporttools descriptors", () => {
20
+ it("validates the format and scope pair of every export", () => {
21
+ expect(exportdescriptorof("csv", "runs")).toEqual({ format: "csv", scope: "runs" });
22
+ expect(exportdescriptorof("json", "extractions").scope).toBe("extractions");
23
+ expect(exportdescriptorof("log", "notes").format).toBe("log");
24
+ expect(exportdescriptorof("jsonl", "audit").format).toBe("jsonl");
25
+ expect(exportdescriptorof("markdown", "extraction").scope).toBe("extraction");
26
+ expect(() => exportdescriptorof("xml", "runs")).toThrow(/csv, json, log, jsonl and markdown/);
27
+ expect(() => exportdescriptorof("csv", "profiles")).toThrow(
28
+ /runs, extractions, notes, session, audit and extraction/,
29
+ );
30
+ });
31
+
32
+ it("escapes csv fields the csv way", () => {
33
+ expect(csvfield("plain")).toBe("plain");
34
+ expect(csvfield('with "quote"')).toBe('"with ""quote"""');
35
+ expect(csvfield("a,b")).toBe('"a,b"');
36
+ expect(csvfield("line\nbreak")).toBe('"line\nbreak"');
37
+ expect(csvof([{ a: "1", b: "2" }, { a: "x,y" }])).toBe('a,b\n1,2\n"x,y",');
38
+ });
39
+ });
40
+
41
+ describe("exporttools records", () => {
42
+ it("honors the mask verdicts in every format: the verdicts mask the fields they hold back", () => {
43
+ const records = [{ name: "email", password: "hunter2", author: "user", note: "plain" }];
44
+ const csv = exportrecords({
45
+ descriptor: exportdescriptorof("csv", "extractions"),
46
+ records: [{ name: "email", password: "[redacted]", author: "user", note: "plain" }],
47
+ shapes: [...defaultmaskshapes],
48
+ });
49
+ expect(csv.result.rows).toBe(1);
50
+ expect(csv.content).toContain("[redacted]");
51
+ expect(csv.content).toContain("plain");
52
+ const json = exportrecords({
53
+ descriptor: exportdescriptorof("json", "extractions"),
54
+ records: [{ name: "email", password: "[redacted]", author: "user", note: "plain" }],
55
+ shapes: [...defaultmaskshapes],
56
+ });
57
+ expect(JSON.parse(json.content)[0].password).toBe("[redacted]");
58
+ expect(JSON.parse(json.content)[0].author).toBe("[redacted]");
59
+ const log = exportrecords({
60
+ descriptor: exportdescriptorof("log", "extractions"),
61
+ records: [{ name: "email", password: "[redacted]", note: "plain" }],
62
+ shapes: [...defaultmaskshapes],
63
+ });
64
+ expect(log.content).toContain("password=[redacted]");
65
+ expect(log.content).toContain("note=plain");
66
+ const usershapes = exportrecords({
67
+ descriptor: exportdescriptorof("csv", "extractions"),
68
+ records: [{ internalcode: "raw" }],
69
+ shapes: [...defaultmaskshapes, "internalcode"],
70
+ });
71
+ expect(usershapes.result.rows).toBe(1);
72
+ expect(usershapes.content).toContain("[redacted]");
73
+ expect(unmaskedfieldsof(records)).toEqual(["password"]);
74
+ });
75
+
76
+ it("refuses to export unmasked values in full under any format", () => {
77
+ const records = [{ name: "email", password: "hunter2" }];
78
+ const refused = exportrecords({
79
+ descriptor: exportdescriptorof("csv", "extractions"),
80
+ records,
81
+ shapes: [...defaultmaskshapes],
82
+ });
83
+ expect(refused.result.rows).toBe(0);
84
+ expect(refused.result.bytes).toBe(0);
85
+ expect(refused.content).toBe("");
86
+ expect(refused.result.reason).toMatch(/refuses 1 unmasked value.*password/);
87
+ expect(unmaskedfieldsof(records)).toEqual(["password"]);
88
+ expect(unmaskedfieldsof([{ apikey: "xyz" }])).toEqual(["apikey"]);
89
+ expect(unmaskedfieldsof([{ authorization: "Bearer raw" }])).toEqual(["authorization"]);
90
+ expect(unmaskedfieldsof([{ password: "[redacted]" }])).toEqual([]);
91
+ expect(unmaskedfieldsof([{ note: "plain" }])).toEqual([]);
92
+ expect(unmaskedfieldsof([{ password: "" }])).toEqual([]);
93
+ });
94
+
95
+ it("exports notes with the sealed bodies intact and the plain bodies masked", () => {
96
+ const notes = [
97
+ {
98
+ id: "n1",
99
+ origin: "https://example.org",
100
+ title: "Release notes",
101
+ author: "user",
102
+ sensitive: true,
103
+ sealedbody: "sealed:abc",
104
+ createdat: now,
105
+ updatedat: now,
106
+ },
107
+ {
108
+ id: "n2",
109
+ origin: "https://example.org",
110
+ title: "Plain note",
111
+ body: "the changelog",
112
+ author: "user",
113
+ sensitive: false,
114
+ createdat: now,
115
+ updatedat: now,
116
+ },
117
+ ];
118
+ const row = noterow(notes[0] as never);
119
+ expect(row.sealedbody).toBe("sealed:abc");
120
+ expect(row.body).toBeUndefined();
121
+ const exported = exportnotes({
122
+ descriptor: exportdescriptorof("json", "notes"),
123
+ notes,
124
+ shapes: [...defaultmaskshapes],
125
+ });
126
+ expect(exported.result.rows).toBe(2);
127
+ expect(exported.content).toContain("sealed:abc");
128
+ expect(exported.content).toContain("Release notes");
129
+ const extractions = exportextractions({
130
+ descriptor: exportdescriptorof("csv", "extractions"),
131
+ rows: [{ title: "row", password: "[redacted]" }],
132
+ shapes: [...defaultmaskshapes],
133
+ });
134
+ expect(extractions.content).toContain("[redacted]");
135
+ const refusedextraction = exportextractions({
136
+ descriptor: exportdescriptorof("csv", "extractions"),
137
+ rows: [{ title: "row", password: "hunter2" }],
138
+ shapes: [...defaultmaskshapes],
139
+ });
140
+ expect(refusedextraction.result.reason).toMatch(/refuses 1 unmasked value/);
141
+ });
142
+ });
143
+
144
+ describe("exporttools chains", () => {
145
+ it("verifies the log chain before any byte leaves the store", async () => {
146
+ let log = openrunlog({ runid: "run:1", sessionid: "flowrun", now });
147
+ log = await appendlogentry({
148
+ log,
149
+ kind: "step",
150
+ summary: "The step ran.",
151
+ origin: "https://example.org",
152
+ stepid: "observe",
153
+ at: now,
154
+ });
155
+ log = await appendlogentry({
156
+ log,
157
+ kind: "flowrun",
158
+ summary: "The flowrun completed.",
159
+ origin: "https://example.org",
160
+ at: now + 10,
161
+ });
162
+ const rows = chainrows(log.entries);
163
+ expect(rows).toHaveLength(2);
164
+ expect(rows[0]).toMatchObject({ kind: "step", origin: "https://example.org", stepid: "observe" });
165
+ const csv = await exportchain({
166
+ descriptor: exportdescriptorof("csv", "runs"),
167
+ log,
168
+ shapes: [...defaultmaskshapes],
169
+ });
170
+ expect(csv.result.rows).toBe(2);
171
+ expect(csv.content.split("\n")[0]).toContain("kind");
172
+ const json = await exportchain({
173
+ descriptor: exportdescriptorof("json", "runs"),
174
+ log,
175
+ shapes: [...defaultmaskshapes],
176
+ });
177
+ expect(JSON.parse(json.content)).toHaveLength(2);
178
+ const second = log.entries[1];
179
+ if (second === undefined) throw new Error("The fixture chain needs its second entry.");
180
+ const broken = { ...log, entries: [...log.entries.slice(0, 1), { ...second, summary: "tampered" }] };
181
+ const refused = await exportchain({ descriptor: exportdescriptorof("csv", "runs"), log: broken, shapes: [] });
182
+ expect(refused.result.rows).toBe(0);
183
+ expect(refused.result.reason).toMatch(/chain|verification/i);
184
+ });
185
+ });
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ firefoxdenylist,
4
+ firefoxpermissionmap,
5
+ firefoxprepoverlay,
6
+ firefoxprepadapt,
7
+ firefoxprepsplitbundle,
8
+ firefoxprepdenylistcheck,
9
+ firefoxactionmap,
10
+ } from "../crossbrowser.js";
11
+ import type { browsermanifestsource, browsermanifestoverlay } from "../types.js";
12
+
13
+ const sourcesmanifest: browsermanifestsource = {
14
+ manifest_version: 3,
15
+ name: "Devthink",
16
+ version: "1.1.86",
17
+ description: "test",
18
+ permissions: ["activeTab", "storage", "scripting", "sidePanel"],
19
+ optional_permissions: ["tabs", "downloads", "clipboardRead", "clipboardWrite", "offscreen", "nativeMessaging"],
20
+ optional_host_permissions: ["https://*/*"],
21
+ background: { service_worker: "background.js", type: "module" },
22
+ action: { default_popup: "popup.html" },
23
+ side_panel: { default_path: "sidepanel.html" },
24
+ options_ui: { page: "optionspage.html", open_in_tab: true },
25
+ offscreen: { document: "offscreen.html", reasons: ["DOM_PARSER", "WORKERS"], justification: "test" },
26
+ };
27
+
28
+ describe("firefoxprep", () => {
29
+ it("writes the browser specific settings with the generated extension id", () => {
30
+ const overlay = firefoxprepoverlay({
31
+ extensionid: "devthink@wenathlan",
32
+ strictminversion: "115.0",
33
+ backgroundscript: "background.js",
34
+ });
35
+ expect(overlay.browser_specific_settings?.id).toBe("devthink@wenathlan");
36
+ expect(overlay.browser_specific_settings?.strict_min_version).toBe("115.0");
37
+ });
38
+
39
+ it("maps action keys to the firefox equivalents", () => {
40
+ expect(firefoxactionmap()).toEqual({
41
+ default_popup: "default_popup",
42
+ default_title: "default_title",
43
+ default_icon: "default_icon",
44
+ });
45
+ });
46
+
47
+ it("moves the service worker to an event page for firefox", () => {
48
+ const overlay = firefoxprepoverlay({ extensionid: "devthink@wenathlan", backgroundscript: "background.js" });
49
+ expect(overlay.background?.scripts).toEqual(["background.js"]);
50
+ expect(overlay.background?.service_worker).toBeUndefined();
51
+ const split = firefoxprepsplitbundle({ backgroundscript: "background.js" });
52
+ expect(split.scripts).toEqual(["background.js"]);
53
+ expect(split.serviceworkerdropped).toBe(true);
54
+ });
55
+
56
+ it("rewrites optional permission names where they differ", () => {
57
+ const map = firefoxpermissionmap();
58
+ expect(map.offscreen).toBe("");
59
+ expect(map.tabs).toBe("tabs");
60
+ expect(map.nativeMessaging).toBe("nativeMessaging");
61
+ });
62
+
63
+ it("keeps host permissions empty on every browser", () => {
64
+ const overlay = firefoxprepoverlay({ extensionid: "devthink@wenathlan", backgroundscript: "background.js" });
65
+ expect(overlay.host_permissions).toEqual([]);
66
+ const adapted = firefoxprepadapt({ manifest: sourcesmanifest, overlay, backgroundscripts: ["background.js"] });
67
+ expect(adapted.manifest.host_permissions).toEqual([]);
68
+ });
69
+
70
+ it("splits the background bundle for event page semantics", () => {
71
+ const overlay = firefoxprepoverlay({ extensionid: "devthink@wenathlan", backgroundscript: "background.js" });
72
+ expect(overlay.background?.scripts).toEqual(["background.js"]);
73
+ });
74
+
75
+ it("keeps the deny list in force across overlays", () => {
76
+ const check = firefoxprepdenylistcheck({ manifest: sourcesmanifest });
77
+ expect(check.ok).toBe(true);
78
+ expect(check.refused).toEqual([]);
79
+ const refused: browsermanifestsource = { ...sourcesmanifest, permissions: ["debugger", "storage"] };
80
+ const refusedcheck = firefoxprepdenylistcheck({ manifest: refused });
81
+ expect(refusedcheck.ok).toBe(false);
82
+ expect(refusedcheck.refused).toEqual(["debugger"]);
83
+ });
84
+
85
+ it("drops the offscreen field and the side_panel field on the firefox manifest", () => {
86
+ const overlay = firefoxprepoverlay({ extensionid: "devthink@wenathlan", backgroundscript: "background.js" });
87
+ const adapted = firefoxprepadapt({ manifest: sourcesmanifest, overlay, backgroundscripts: ["background.js"] });
88
+ expect(adapted.manifest.offscreen).toBeUndefined();
89
+ expect(adapted.manifest.side_panel).toBeUndefined();
90
+ expect(adapted.changes.length).toBeGreaterThan(5);
91
+ });
92
+
93
+ it("writes the browser_specific_settings gecko block with the extension id", () => {
94
+ const overlay = firefoxprepoverlay({
95
+ extensionid: "devthink@wenathlan",
96
+ strictminversion: "115.0",
97
+ backgroundscript: "background.js",
98
+ });
99
+ const adapted = firefoxprepadapt({ manifest: sourcesmanifest, overlay, backgroundscripts: ["background.js"] });
100
+ expect(adapted.manifest.browser_specific_settings?.gecko?.id).toBe("devthink@wenathlan");
101
+ expect(adapted.manifest.browser_specific_settings?.gecko?.strict_min_version).toBe("115.0");
102
+ });
103
+
104
+ it("refuses an overlay for the wrong browser", () => {
105
+ const wrong: browsermanifestoverlay = { browser: "safari" };
106
+ expect(() =>
107
+ firefoxprepadapt({ manifest: sourcesmanifest, overlay: wrong, backgroundscripts: ["background.js"] }),
108
+ ).toThrow();
109
+ });
110
+
111
+ it("refuses a manifest with required host permissions", () => {
112
+ const overlay = firefoxprepoverlay({ extensionid: "devthink@wenathlan", backgroundscript: "background.js" });
113
+ const withHosts: browsermanifestsource = { ...sourcesmanifest, host_permissions: ["https://example.com/*"] };
114
+ expect(() => firefoxprepadapt({ manifest: withHosts, overlay, backgroundscripts: ["background.js"] })).toThrow();
115
+ });
116
+
117
+ it("removes the offscreen optional permission for firefox", () => {
118
+ const overlay = firefoxprepoverlay({ extensionid: "devthink@wenathlan", backgroundscript: "background.js" });
119
+ const adapted = firefoxprepadapt({ manifest: sourcesmanifest, overlay, backgroundscripts: ["background.js"] });
120
+ expect(adapted.manifest.optional_permissions).not.toContain("offscreen");
121
+ expect(adapted.manifest.optional_permissions).toContain("tabs");
122
+ expect(adapted.manifest.optional_permissions).toContain("nativeMessaging");
123
+ });
124
+
125
+ it("declares the firefox deny list of forbidden permissions", () => {
126
+ expect(firefoxdenylist).toContain("debugger");
127
+ expect(firefoxdenylist).toContain("cookies");
128
+ expect(firefoxdenylist).toContain("webRequest");
129
+ expect(firefoxdenylist).toContain("history");
130
+ expect(firefoxdenylist).toContain("bookmarks");
131
+ expect(firefoxdenylist).toContain("proxy");
132
+ expect(firefoxdenylist).toContain("management");
133
+ });
134
+ });