@wenathlan/devthink 2.0.13 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ apimapentries,
4
+ apimapentryof,
5
+ apimapkinds,
6
+ apimapresolve,
7
+ apimapunmapped,
8
+ apimapbrowserof,
9
+ apimapbrowsercacheprobe,
10
+ apifeatureflagintersection,
11
+ apimapstructerrorof,
12
+ } from "../crossbrowser.js";
13
+
14
+ describe("apimap catalog", () => {
15
+ it("records every webextension api the codebase touches with a chromium and firefox mapping", () => {
16
+ const entries = apimapentries();
17
+ expect(entries.length).toBeGreaterThan(20);
18
+ for (const entry of entries) {
19
+ expect(entry.chromium).not.toBe("");
20
+ expect(entry.firefox).not.toBe("");
21
+ expect(entry.api).not.toBe("");
22
+ }
23
+ });
24
+
25
+ it("resolves the right call through the runtime browser probe", () => {
26
+ const chromium = apimapresolve({ api: "runtime.geturl", runtime: { chrome: { runtime: { id: "abc" } } } });
27
+ expect(chromium.ok).toBe(true);
28
+ expect(chromium.browser).toBe("chromium");
29
+ expect(chromium.equivalent).toBe("chrome.runtime.getURL");
30
+ const firefox = apimapresolve({ api: "runtime.geturl", runtime: { browser: { runtime: { id: "abc" } } } });
31
+ expect(firefox.ok).toBe(true);
32
+ expect(firefox.browser).toBe("firefox");
33
+ expect(firefox.equivalent).toBe("browser.runtime.getURL");
34
+ });
35
+
36
+ it("reports unmapped apis at build time and fails the build when a new api lacks a row", () => {
37
+ const report = apimapunmapped();
38
+ expect(report.failed).toBe(false);
39
+ expect(report.missingchromium.length).toBe(0);
40
+ expect(report.missingfirefox.length).toBe(0);
41
+ const partial = apimapunmapped([
42
+ ...apimapentries(),
43
+ { api: "fictional.api", chromium: "", firefox: "", safari: "", kind: "method" },
44
+ ]);
45
+ expect(partial.failed).toBe(true);
46
+ expect(partial.missingchromium).toContain("fictional.api");
47
+ expect(partial.missingfirefox).toContain("fictional.api");
48
+ });
49
+
50
+ it("looks up one apimap entry by its reviewed api name", () => {
51
+ const entry = apimapentryof("runtime.geturl");
52
+ expect(entry?.api).toBe("runtime.geturl");
53
+ expect(apimapentryof("nonexistent.api")).toBeUndefined();
54
+ });
55
+
56
+ it("declares the four kind categories", () => {
57
+ const kinds = apimapkinds();
58
+ expect(kinds).toEqual(["namespace", "method", "event", "property"]);
59
+ });
60
+
61
+ it("caches the detected browser in memory across calls", () => {
62
+ const probe = apimapbrowsercacheprobe({ runtime: { chrome: { runtime: { id: "abc" } } } });
63
+ expect(probe.probe()).toBe("chromium");
64
+ expect(probe.probe()).toBe("chromium");
65
+ const refreshed = probe.refresh();
66
+ expect(refreshed).toBe("chromium");
67
+ });
68
+
69
+ it("surfaces unmapped apis as structured errors with the retry hint none", () => {
70
+ const resolution = apimapresolve({ api: "runtime.geturl", browser: "safari" });
71
+ expect(resolution.ok).toBe(true);
72
+ const struct = apimapstructerrorof({ api: "fictional.api", browser: "safari", reason: "no mapping", now: 1000 });
73
+ expect(struct.family).toBe("apimap");
74
+ expect(struct.retry).toBe("none");
75
+ expect(struct.browser).toBe("safari");
76
+ expect(struct.api).toBe("fictional.api");
77
+ expect(struct.at).toBe(1000);
78
+ });
79
+
80
+ it("defaults the feature flag set to the intersection across chromium, firefox and safari", () => {
81
+ const intersection = apifeatureflagintersection();
82
+ expect(intersection).toContain("runtime.geturl");
83
+ expect(intersection).toContain("tabs.query");
84
+ expect(intersection).toContain("scripting.executeScript");
85
+ });
86
+
87
+ it("resolves the sidepanel api to the polyfill fallback on firefox", () => {
88
+ const firefox = apimapresolve({ api: "sidePanel.open", browser: "firefox" });
89
+ expect(firefox.ok).toBe(true);
90
+ expect(firefox.equivalent).toBe("browserpolyfill.sidepanel.open");
91
+ const safari = apimapresolve({ api: "offscreen.createDocument", browser: "safari" });
92
+ expect(safari.ok).toBe(true);
93
+ expect(safari.equivalent).toBe("browserpolyfill.offscreen.inline");
94
+ });
95
+
96
+ it("probes the running browser from the user agent when the runtime seam is absent", () => {
97
+ expect(
98
+ apimapbrowserof({
99
+ probeuseragent: () => "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
100
+ }),
101
+ ).toBe("firefox");
102
+ expect(
103
+ apimapbrowserof({ probeuseragent: () => "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) Safari/605.1.15" }),
104
+ ).toBe("safari");
105
+ expect(
106
+ apimapbrowserof({
107
+ probeuseragent: () =>
108
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
109
+ }),
110
+ ).toBe("chromium");
111
+ });
112
+ });
@@ -0,0 +1,175 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ approvalprompt,
4
+ defaultapprovalwindowms,
5
+ expireapprovals,
6
+ listapprovals,
7
+ redactparams,
8
+ requireapproval,
9
+ resolveapproval,
10
+ } from "../gates.js";
11
+ import { consentmetagrade } from "../policy.js";
12
+ import { buildtoolcatalog } from "../tools.js";
13
+ import type { approvalrequest } from "../types.js";
14
+
15
+ const now = 1_800_000_000_000;
16
+ const catalog = buildtoolcatalog();
17
+
18
+ describe("approval gate lifecycle", () => {
19
+ it("raises gates for sensitive tool calls with the full arguments and timeout", () => {
20
+ const gate = requireapproval({
21
+ clientid: "client1",
22
+ tool: "browser.click",
23
+ reason: "The click runs only as the approved plan step it names.",
24
+ params: { stepid: "s1", target: "#go" },
25
+ now,
26
+ timeout: 5000,
27
+ id: "gate-1",
28
+ });
29
+ expect(gate.id).toBe("gate-1");
30
+ expect(gate.state).toBe("pending");
31
+ expect(gate.raisedat).toBe(now);
32
+ expect(gate.timeoutat).toBe(now + 5000);
33
+ expect(gate.params).toEqual({ stepid: "s1", target: "#go" });
34
+ const secretgate = requireapproval({
35
+ clientid: "client1",
36
+ tool: "browser.type",
37
+ reason: "Typing.",
38
+ params: { stepid: "s2", value: "hunter2" },
39
+ now,
40
+ secretfields: ["value"],
41
+ });
42
+ expect(secretgate.secretfields).toEqual(["value"]);
43
+ expect(secretgate.timeoutat).toBeUndefined();
44
+ expect(defaultapprovalwindowms).toBe(120_000);
45
+ });
46
+
47
+ it("resolves pending gates with the actor and latency while closed gates stay closed", () => {
48
+ const pending = requireapproval({
49
+ clientid: "client1",
50
+ tool: "browser.click",
51
+ reason: "Click.",
52
+ params: { stepid: "s1" },
53
+ now,
54
+ id: "gate-1",
55
+ });
56
+ const approved = resolveapproval({
57
+ requests: [pending],
58
+ id: "gate-1",
59
+ decision: "approved",
60
+ actor: "user",
61
+ now: now + 4000,
62
+ });
63
+ expect(approved.exec?.decision).toBe("approved");
64
+ expect(approved.exec?.actor).toBe("user");
65
+ expect(approved.exec?.latencyms).toBe(4000);
66
+ expect(approved.requests[0]?.state).toBe("approved");
67
+ expect(approved.requests[0]?.decidedat).toBe(now + 4000);
68
+ const again = resolveapproval({
69
+ requests: approved.requests,
70
+ id: "gate-1",
71
+ decision: "refused",
72
+ actor: "user",
73
+ now: now + 5000,
74
+ });
75
+ expect(again.exec).toBeUndefined();
76
+ expect(again.requests[0]?.state).toBe("approved");
77
+ const refused = resolveapproval({
78
+ requests: [pending],
79
+ id: "gate-1",
80
+ decision: "refused",
81
+ actor: "user",
82
+ now: now + 1500,
83
+ });
84
+ expect(refused.exec?.decision).toBe("refused");
85
+ expect(refused.exec?.latencyms).toBe(1500);
86
+ expect(
87
+ resolveapproval({ requests: [pending], id: "missing", decision: "approved", actor: "user", now }).exec,
88
+ ).toBeUndefined();
89
+ });
90
+
91
+ it("expires unanswered gates past their window with the default refusal", () => {
92
+ const short = requireapproval({
93
+ clientid: "client1",
94
+ tool: "browser.click",
95
+ reason: "Click.",
96
+ params: {},
97
+ now,
98
+ timeout: 1000,
99
+ id: "gate-1",
100
+ });
101
+ const endless = requireapproval({
102
+ clientid: "client2",
103
+ tool: "browser.type",
104
+ reason: "Type.",
105
+ params: {},
106
+ now,
107
+ id: "gate-2",
108
+ });
109
+ const swept = expireapprovals([short, endless], now + 1500);
110
+ expect(swept[0]?.state).toBe("expired");
111
+ expect(swept[1]?.state).toBe("pending");
112
+ const resolved = expireapprovals([{ ...short, state: "approved" }], now + 1500);
113
+ expect(resolved[0]?.state).toBe("approved");
114
+ expect(listapprovals([endless, short])[0]?.id).toBe("gate-2");
115
+ });
116
+ });
117
+
118
+ describe("approval prompts and secret redaction", () => {
119
+ it("redacts the fields the user marked secret while the rest stays visible", () => {
120
+ expect(redactparams({ stepid: "s1", value: "hunter2", target: "#go" }, ["value"])).toEqual({
121
+ stepid: "s1",
122
+ value: "[redacted]",
123
+ target: "#go",
124
+ });
125
+ expect(redactparams({ stepid: "s1" }, [])).toEqual({ stepid: "s1" });
126
+ expect(redactparams({ value: "hunter2" }, ["value"]).value).not.toContain("hunter");
127
+ });
128
+
129
+ it("carries the client identity, the called tool and the redacted arguments in every prompt", () => {
130
+ const gate = requireapproval({
131
+ clientid: "client1",
132
+ tool: "browser.type",
133
+ reason: "The typing runs only as the approved plan step it names.",
134
+ params: { stepid: "s1", value: "hunter2" },
135
+ now,
136
+ secretfields: ["value"],
137
+ });
138
+ const prompt = approvalprompt(gate, { fingerprint: "fp-1", displayname: "Laptop agent" });
139
+ expect(prompt).toContain("Laptop agent");
140
+ expect(prompt).toContain("fp-1");
141
+ expect(prompt).toContain("browser.type");
142
+ expect(prompt).toContain("[redacted]");
143
+ expect(prompt).not.toContain("hunter2");
144
+ const bare = approvalprompt(gate);
145
+ expect(bare).toContain("client1");
146
+ expect(bare).not.toContain("hunter2");
147
+ });
148
+ });
149
+
150
+ describe("sensitive tool gating", () => {
151
+ it("grades every sensitive tool with the approval gate requirement and passes read tools free", () => {
152
+ for (const tool of catalog.domains.flatMap((domain) => domain.tools)) {
153
+ const grade = consentmetagrade(tool);
154
+ expect(grade.allowed).toBe(true);
155
+ if (tool.risk !== "read") {
156
+ expect(tool.consentmeta?.approvalrequired).toBe(true);
157
+ expect(tool.consentmeta?.originscope).toBe("session");
158
+ expect(tool.consentmeta?.riskclass).toBe(tool.risk);
159
+ }
160
+ }
161
+ const drift = {
162
+ ...catalog.domains[0]!.tools[9]!,
163
+ consentmeta: {
164
+ review: "review",
165
+ riskclass: "read" as const,
166
+ approvalrequired: false,
167
+ originscope: "session" as const,
168
+ },
169
+ };
170
+ expect(consentmetagrade(drift).allowed).toBe(false);
171
+ const { consentmeta: dropped, ...missing } = catalog.domains[0]!.tools[9]!;
172
+ void dropped;
173
+ expect(consentmetagrade(missing).allowed).toBe(false);
174
+ });
175
+ });
@@ -0,0 +1,204 @@
1
+ import { createHash } from "node:crypto";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ artifactchannelof,
5
+ artifactchannels,
6
+ artifactmanifestcheck,
7
+ artifactmanifestnameof,
8
+ artifactmanifestof,
9
+ artifactmanifesttext,
10
+ rollbackpinnedset,
11
+ } from "../pack.js";
12
+
13
+ const checksumof = (text: string) => createHash("sha256").update(text, "utf8").digest("hex");
14
+ const valid = "a".repeat(64);
15
+
16
+ describe("artifactmanifest", () => {
17
+ it("records the name, the size and the sha256 checksum of every artifact with its channels", () => {
18
+ const manifest = artifactmanifestof({
19
+ version: "1.1.87",
20
+ artifacts: [{ name: "devthink1.1.87.zip", size: 1024, checksum: valid }],
21
+ });
22
+ expect(manifest.name).toBe("devthink artifact manifest");
23
+ expect(manifest.version).toBe("1.1.87");
24
+ expect(manifest.banner).toContain("devthink 1.1.87");
25
+ expect(manifest.banner).toContain("GPL-3.0-only");
26
+ expect(manifest.artifacts[0]).toEqual({
27
+ name: "devthink1.1.87.zip",
28
+ size: 1024,
29
+ checksum: valid,
30
+ channels: ["github", "chromium", "nuget", "maven"],
31
+ });
32
+ });
33
+
34
+ it("sorts the artifacts by name so the document renders deterministically", () => {
35
+ const manifest = artifactmanifestof({
36
+ version: "1.1.87",
37
+ artifacts: [
38
+ { name: "devthink-site-1.1.87.zip", size: 5, checksum: valid },
39
+ { name: "devthink-vscode-1.1.87.vsix", size: 6, checksum: valid },
40
+ { name: "devthink1.1.87.zip", size: 4, checksum: valid },
41
+ ],
42
+ });
43
+ expect(manifest.artifacts.map((entry) => entry.name)).toEqual([
44
+ "devthink-site-1.1.87.zip",
45
+ "devthink-vscode-1.1.87.vsix",
46
+ "devthink1.1.87.zip",
47
+ ]);
48
+ /* the text of the same set renders byte identical on every run because no timestamp enters the document */
49
+ expect(artifactmanifesttext(manifest)).toBe(
50
+ artifactmanifesttext(
51
+ artifactmanifestof({
52
+ version: "1.1.87",
53
+ artifacts: [
54
+ { name: "devthink-site-1.1.87.zip", size: 5, checksum: valid },
55
+ { name: "devthink-vscode-1.1.87.vsix", size: 6, checksum: valid },
56
+ { name: "devthink1.1.87.zip", size: 4, checksum: valid },
57
+ ],
58
+ }),
59
+ ),
60
+ );
61
+ });
62
+
63
+ it("refuses an empty artifact set, a duplicate name and a non sha256 checksum", () => {
64
+ expect(() => artifactmanifestof({ version: "1.1.87", artifacts: [] })).toThrow();
65
+ expect(() =>
66
+ artifactmanifestof({
67
+ version: "1.1.87",
68
+ artifacts: [
69
+ { name: "a.zip", size: 1, checksum: valid },
70
+ { name: "a.zip", size: 1, checksum: valid },
71
+ ],
72
+ }),
73
+ ).toThrow();
74
+ expect(() =>
75
+ artifactmanifestof({ version: "1.1.87", artifacts: [{ name: "a.zip", size: 1, checksum: "short" }] }),
76
+ ).toThrow();
77
+ expect(() =>
78
+ artifactmanifestof({ version: "1.1.87", artifacts: [{ name: "", size: 1, checksum: valid }] }),
79
+ ).toThrow();
80
+ expect(() =>
81
+ artifactmanifestof({ version: "not-a-version", artifacts: [{ name: "a.zip", size: 1, checksum: valid }] }),
82
+ ).toThrow();
83
+ });
84
+
85
+ it("resolves the publishing channels of the release artifact names", () => {
86
+ expect(artifactchannels()).toEqual([
87
+ "github",
88
+ "npmjs",
89
+ "githubpackages",
90
+ "nuget",
91
+ "maven",
92
+ "container",
93
+ "vscode",
94
+ "firefox",
95
+ "safari",
96
+ "chromium",
97
+ "site",
98
+ "declarations",
99
+ "provenance",
100
+ ]);
101
+ expect(artifactchannelof("wenathlan-extension-1.1.87.tgz", "1.1.87")).toContain("npmjs");
102
+ expect(artifactchannelof("extension.1.1.87.nupkg", "1.1.87")).toContain("nuget");
103
+ expect(artifactchannelof("extension-1.1.87.pom", "1.1.87")).toContain("maven");
104
+ expect(artifactchannelof("extension-container.json", "1.1.87")).toContain("container");
105
+ expect(artifactchannelof("devthink-vscode-1.1.87.vsix", "1.1.87")).toContain("vscode");
106
+ expect(artifactchannelof("devthink-firefox-1.1.87.xpi", "1.1.87")).toContain("firefox");
107
+ expect(artifactchannelof("devthink-safari-1.1.87.zip", "1.1.87")).toContain("safari");
108
+ expect(artifactchannelof("devthink1.1.87.zip", "1.1.87")).toContain("chromium");
109
+ expect(artifactchannelof("devthink-site-1.1.87.zip", "1.1.87")).toContain("site");
110
+ expect(artifactchannelof("devthink-declarations-1.1.87.zip", "1.1.87")).toContain("declarations");
111
+ expect(artifactchannelof("devthink-sbom-1.1.87.json", "1.1.87")).toContain("provenance");
112
+ /* an unrecognized name still rides the github asset channel because every release artifact attaches to the release */
113
+ expect(artifactchannelof("unknown-artifact.bin", "1.1.87")).toEqual(["github"]);
114
+ /* the dist bundles ride the npm channels */
115
+ expect(artifactchannelof("dist/index.js", "1.1.87")).toEqual(["npmjs", "githubpackages"]);
116
+ });
117
+
118
+ it("names the manifest asset with the release version", () => {
119
+ expect(artifactmanifestnameof("1.1.87")).toBe("devthink-artifactmanifest-1.1.87.json");
120
+ expect(() => artifactmanifestnameof("not-a-version")).toThrow();
121
+ });
122
+
123
+ it("verifies a built or downloaded set matches the manifest exactly", () => {
124
+ const manifest = artifactmanifestof({
125
+ version: "1.1.87",
126
+ artifacts: [
127
+ { name: "devthink1.1.87.zip", size: 10, checksum: checksumof("one") },
128
+ { name: "devthink-site-1.1.87.zip", size: 20, checksum: checksumof("two") },
129
+ ],
130
+ });
131
+ const okcheck = artifactmanifestcheck({
132
+ manifest,
133
+ files: [
134
+ { name: "devthink1.1.87.zip", checksum: checksumof("one") },
135
+ { name: "devthink-site-1.1.87.zip", checksum: checksumof("two") },
136
+ ],
137
+ });
138
+ expect(okcheck.ok).toBe(true);
139
+ /* a missing artifact, a mismatched checksum and an unexpected extra file each fail the check */
140
+ const missing = artifactmanifestcheck({
141
+ manifest,
142
+ files: [{ name: "devthink-site-1.1.87.zip", checksum: checksumof("two") }],
143
+ });
144
+ expect(missing.ok).toBe(false);
145
+ expect(missing.missing).toEqual(["devthink1.1.87.zip"]);
146
+ const mismatched = artifactmanifestcheck({
147
+ manifest,
148
+ files: [
149
+ { name: "devthink1.1.87.zip", checksum: checksumof("changed") },
150
+ { name: "devthink-site-1.1.87.zip", checksum: checksumof("two") },
151
+ ],
152
+ });
153
+ expect(mismatched.ok).toBe(false);
154
+ expect(mismatched.mismatched).toEqual(["devthink1.1.87.zip"]);
155
+ const unexpected = artifactmanifestcheck({
156
+ manifest,
157
+ files: [
158
+ { name: "devthink1.1.87.zip", checksum: checksumof("one") },
159
+ { name: "devthink-site-1.1.87.zip", checksum: checksumof("two") },
160
+ { name: "stray.bin", checksum: checksumof("stray") },
161
+ ],
162
+ });
163
+ expect(unexpected.ok).toBe(false);
164
+ expect(unexpected.unexpected).toEqual(["stray.bin"]);
165
+ /* the checksums file itself stays ignorable beside the manifest */
166
+ const withsums = artifactmanifestcheck({
167
+ manifest,
168
+ files: [
169
+ { name: "devthink1.1.87.zip", checksum: checksumof("one") },
170
+ { name: "devthink-site-1.1.87.zip", checksum: checksumof("two") },
171
+ { name: "SHA256SUMS.txt", checksum: checksumof("sums") },
172
+ ],
173
+ });
174
+ expect(withsums.ok).toBe(true);
175
+ });
176
+
177
+ it("freezes the rollback pin of the previous release artifact set", () => {
178
+ const previous = artifactmanifestof({
179
+ version: "1.1.86",
180
+ artifacts: [{ name: "devthink1.1.86.zip", size: 10, checksum: checksumof("previous") }],
181
+ });
182
+ const pin = rollbackpinnedset({ previousversion: "1.1.86", manifest: previous });
183
+ expect(pin.tag).toBe("v1.1.86");
184
+ expect(pin.version).toBe("1.1.86");
185
+ expect(pin.artifacts).toEqual([
186
+ {
187
+ name: "devthink1.1.86.zip",
188
+ checksum: checksumof("previous"),
189
+ channels: ["github", "chromium", "nuget", "maven"],
190
+ },
191
+ ]);
192
+ expect(pin.procedure).toEqual([
193
+ "checkout the immutable tag v1.1.86",
194
+ "download the release assets of that tag",
195
+ "verify every asset against the pinned checksums of this pin",
196
+ "republish the pinned asset set on the channels the rollback covers",
197
+ ]);
198
+ /* the pin carries no download url, only the immutable tag and the digests */
199
+ expect(JSON.stringify(pin)).not.toMatch(/https?:\/\//);
200
+ /* a manifest of another version never freezes a pin */
201
+ expect(() => rollbackpinnedset({ previousversion: "1.1.85", manifest: previous })).toThrow();
202
+ expect(() => rollbackpinnedset({ previousversion: "not-a-version", manifest: previous })).toThrow();
203
+ });
204
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ attentioncountof,
4
+ attentiondeeplinkof,
5
+ attentionentryof,
6
+ attentionnotifications,
7
+ collectattention,
8
+ dedupeattention,
9
+ dismissattention,
10
+ pruneattention,
11
+ rankattention,
12
+ attentionseverityof,
13
+ } from "../views.js";
14
+
15
+ const now = 1_800_000_000_000;
16
+
17
+ describe("attentionfeed collection and ranking", () => {
18
+ it("collects gate waits, phishguard blocks, deferrals and failures with their deep links", () => {
19
+ const entries = collectattention({
20
+ gatewaits: [
21
+ {
22
+ runid: "run1",
23
+ stepid: "step1",
24
+ gateid: "gate1",
25
+ kind: "confirmpay",
26
+ origin: "https://example.com",
27
+ waitedms: 5000,
28
+ },
29
+ ],
30
+ phishblocks: [
31
+ {
32
+ runid: "run1",
33
+ origin: "https://lookalike.test",
34
+ matchedorigin: "https://example.com",
35
+ reason: "distance 0.9 over the threshold",
36
+ },
37
+ ],
38
+ deferrals: [{ runid: "run2", origin: "https://example.com", reason: "the bucket reset waits" }],
39
+ failures: [
40
+ { runid: "run3", stepid: "step9", origin: "https://example.com", message: "the selector matched nothing" },
41
+ ],
42
+ now,
43
+ });
44
+ expect(entries.map((entry) => entry.cause)).toEqual(["gatewait", "phishguard", "deferral", "failure"]);
45
+ expect(entries[0]?.deeplink).toBe("devthink://gate/gate1?run=run1");
46
+ expect(entries[1]?.deeplink).toBe("devthink://phishguard?run=run1");
47
+ expect(entries[2]?.deeplink).toBe("devthink://deferred?run=run2");
48
+ expect(entries[3]?.deeplink).toBe("devthink://error?run=run3");
49
+ expect(() =>
50
+ attentionentryof({ cause: "gatewait", runid: " ", origin: "https://example.com", summary: "waits", at: now }),
51
+ ).toThrow(/run ref/);
52
+ expect(() =>
53
+ attentionentryof({ cause: "gatewait", runid: "run1", origin: "https://example.com", summary: " ", at: now }),
54
+ ).toThrow(/summary/);
55
+ });
56
+
57
+ it("ranks the causes by severity first and time second", () => {
58
+ const entries = [
59
+ attentionentryof({
60
+ cause: "failure",
61
+ runid: "r",
62
+ origin: "https://example.com",
63
+ summary: "failed late",
64
+ at: now + 5000,
65
+ }),
66
+ attentionentryof({
67
+ cause: "deferral",
68
+ runid: "r",
69
+ origin: "https://example.com",
70
+ summary: "deferred",
71
+ at: now + 1000,
72
+ }),
73
+ attentionentryof({
74
+ cause: "gatewait",
75
+ runid: "r",
76
+ origin: "https://example.com",
77
+ summary: "waits",
78
+ gateref: "g1",
79
+ at: now,
80
+ }),
81
+ attentionentryof({
82
+ cause: "phishguard",
83
+ runid: "r",
84
+ origin: "https://example.com",
85
+ summary: "blocked",
86
+ at: now + 2000,
87
+ }),
88
+ ];
89
+ const ranked = rankattention(entries);
90
+ expect(ranked.map((entry) => entry.cause)).toEqual(["phishguard", "gatewait", "deferral", "failure"]);
91
+ expect(attentionseverityof("gatewait")).toBe("critical");
92
+ expect(attentionseverityof("phishguard")).toBe("critical");
93
+ expect(attentionseverityof("deferral")).toBe("warning");
94
+ expect(attentionseverityof("failure")).toBe("info");
95
+ });
96
+
97
+ it("deduplicates repeated causes per run while the first occurrence keeps its time", () => {
98
+ const first = attentionentryof({
99
+ cause: "gatewait",
100
+ runid: "r",
101
+ origin: "https://example.com",
102
+ summary: "waits",
103
+ gateref: "g1",
104
+ at: now,
105
+ });
106
+ const second = attentionentryof({
107
+ cause: "gatewait",
108
+ runid: "r",
109
+ origin: "https://example.com",
110
+ summary: "still waits",
111
+ gateref: "g1",
112
+ at: now + 9000,
113
+ });
114
+ const othergate = attentionentryof({
115
+ cause: "gatewait",
116
+ runid: "r",
117
+ origin: "https://example.com",
118
+ summary: "waits",
119
+ gateref: "g2",
120
+ at: now + 1000,
121
+ });
122
+ const deduped = dedupeattention([second, first, othergate]);
123
+ expect(deduped).toHaveLength(2);
124
+ expect(deduped[0]?.at).toBe(now);
125
+ expect(attentioncountof([first, second])).toBe(1);
126
+ });
127
+
128
+ it("dismisses entries and prunes past the retention window", () => {
129
+ const entry = attentionentryof({
130
+ cause: "failure",
131
+ runid: "r",
132
+ origin: "https://example.com",
133
+ summary: "failed",
134
+ at: now,
135
+ });
136
+ expect(dismissattention([entry], entry.id)).toHaveLength(0);
137
+ const pruned = pruneattention([entry], 1000, now + 2000);
138
+ expect(pruned.kept).toHaveLength(0);
139
+ expect(pruned.pruned).toEqual([entry.id]);
140
+ expect(pruneattention([entry], undefined, now + 2000).kept).toHaveLength(1);
141
+ expect(pruneattention([entry], 1000, now + 500).kept).toHaveLength(1);
142
+ });
143
+
144
+ it("renders the entries as content free system notifications with their deep links", () => {
145
+ const entries = [
146
+ attentionentryof({
147
+ cause: "gatewait",
148
+ runid: "r",
149
+ origin: "https://example.com",
150
+ summary: "waits",
151
+ gateref: "g1",
152
+ at: now,
153
+ }),
154
+ ];
155
+ const notifications = attentionnotifications(entries, now);
156
+ expect(notifications).toHaveLength(1);
157
+ expect(notifications[0]?.kind).toBe("attention");
158
+ expect(notifications[0]?.title).toBe("A gate waits for you");
159
+ expect(notifications[0]?.content).toBe(false);
160
+ expect(notifications[0]?.deeplink).toBe("devthink://gate/g1?run=r");
161
+ expect(attentiondeeplinkof("deferral", "r")).toBe("devthink://deferred?run=r");
162
+ });
163
+ });