@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,414 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ cleanupafterrun,
4
+ enforcequarantine,
5
+ encryptsync,
6
+ exportall,
7
+ fixedtelemetrypolicy,
8
+ jarscope,
9
+ localfieldsof,
10
+ localfirst,
11
+ newjar,
12
+ optinsync,
13
+ purgeonrequest,
14
+ sealjar,
15
+ streambundle,
16
+ syncpass,
17
+ type keyderive,
18
+ type payloadencrypt,
19
+ type syncsend,
20
+ } from "../export.js";
21
+ import {
22
+ cleanupgate,
23
+ encryptsyncgate,
24
+ exportallgate,
25
+ jargate,
26
+ localgate,
27
+ minimizationstripgrade,
28
+ notelemetryinvariant,
29
+ purgegate,
30
+ quarantineopengate,
31
+ syncgate,
32
+ } from "../policy.js";
33
+ import {
34
+ outboundpayloadcheck,
35
+ quarantineverdictreport,
36
+ requestbody,
37
+ syncconsentreport,
38
+ syncpayloadreport,
39
+ } from "../protocol.js";
40
+ import type { datainventory, localrule, purgepolicy, syncsettings } from "../types.js";
41
+
42
+ const now = 1_800_000_000_000;
43
+
44
+ /** Wires plain fixture seams: the key derivation uppercases the passphrase marker and the cipher tags the payload so the tests read what the seams carried. */
45
+ const derive: keyderive = async (passphrase: string) => `key-${passphrase.length}`;
46
+ const encrypt: payloadencrypt = async (key: string, payload: string) => `${key}:${payload.length}`;
47
+
48
+ describe("minimization localfirst stripping and localrule blocking", () => {
49
+ it("keeps the extraction on the device, strips the identity fields the review never listed and holds the local rule fields from every outbound payload", () => {
50
+ const rules: localrule[] = [
51
+ { origin: "https://shop.example", fields: ["sessiontoken", "cartsecret"], at: now - 1000 },
52
+ ];
53
+ const pass = localfirst({
54
+ origin: "https://shop.example",
55
+ rows: [
56
+ { title: "pricing table", email: "buyer@example.org", userid: "u-17", sessiontoken: "st-abc", total: "42" },
57
+ { title: "shipping table", phone: "+1 555 0100", total: "7" },
58
+ ],
59
+ rules,
60
+ now,
61
+ });
62
+ expect(pass.rows).toEqual([
63
+ { title: "pricing table", total: "42" },
64
+ { title: "shipping table", total: "7" },
65
+ ]);
66
+ expect(pass.stripped.sort()).toEqual(["email", "phone", "userid"]);
67
+ expect(pass.localheld.sort()).toEqual(["sessiontoken"]);
68
+ expect(pass.reason).toContain("held 1 local rule field");
69
+ const reviewed = localfirst({
70
+ origin: "https://shop.example",
71
+ rows: [{ email: "buyer@example.org", total: "42" }],
72
+ reviewedfields: ["email", "total"],
73
+ rules,
74
+ now,
75
+ });
76
+ expect(reviewed.rows).toEqual([{ email: "buyer@example.org", total: "42" }]);
77
+ expect(reviewed.stripped).toEqual([]);
78
+ expect(localfieldsof({ sessiontoken: "x", total: "42" } as Record<string, unknown>, rules)).toEqual([
79
+ "sessiontoken",
80
+ ]);
81
+ expect(localgate({ payload: { sessiontoken: "x", total: "42" }, rules }).allowed).toBe(false);
82
+ expect(localgate({ payload: { total: "42" }, rules }).allowed).toBe(true);
83
+ expect(() => localfirst({ origin: " ", rows: [], rules: [], now })).toThrow(/names its origin/);
84
+ expect(outboundpayloadcheck({ payload: { total: "42" }, localfields: ["sessiontoken"] }).ok).toBe(true);
85
+ expect(() => outboundpayloadcheck({ payload: { sessiontoken: "x" }, localfields: ["sessiontoken"] })).toThrow(
86
+ /never leaves the device/,
87
+ );
88
+ expect(minimizationstripgrade({ fields: ["title", "total"], reviewed: ["title", "total"] }).allowed).toBe(true);
89
+ expect(minimizationstripgrade({ fields: ["title", "email"], reviewed: ["title"] }).allowed).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe("minimization notelemetry invariant with zero outbound calls", () => {
94
+ it("keeps every counter inside the local memory while the worker issues no outbound usage call at all", async () => {
95
+ const policy = fixedtelemetrypolicy(now);
96
+ expect(policy.enabled).toBe(false);
97
+ expect(policy.counters).toBe("local");
98
+ const typedoff: { enabled: false; counters: "local"; at: number } = policy;
99
+ expect(typedoff.enabled).toBe(false);
100
+ let outboundcalls = 0;
101
+ const counting: syncsend = async () => {
102
+ outboundcalls += 1;
103
+ return { delivered: false };
104
+ };
105
+ await counting("no telemetry payload ships", []);
106
+ expect(outboundcalls).toBe(1);
107
+ outboundcalls = 0;
108
+ expect(notelemetryinvariant({ outboundcalls }).allowed).toBe(true);
109
+ expect(notelemetryinvariant({ outboundcalls: 1 }).allowed).toBe(false);
110
+ const wire = JSON.parse(
111
+ requestbody({
112
+ objective: "collect the pricing table",
113
+ session: {
114
+ id: "s1",
115
+ tabid: 1,
116
+ origin: "https://shop.example",
117
+ startedat: now,
118
+ expiresat: now + 1000,
119
+ jarid: "jar-1",
120
+ },
121
+ observation: {
122
+ schemaversion: 1,
123
+ url: "https://shop.example",
124
+ title: "Shop",
125
+ textpreview: "",
126
+ textlength: 0,
127
+ forms: [],
128
+ interactive: [],
129
+ capturedat: now,
130
+ },
131
+ capabilities: { tabs: true, downloads: true, clipboardread: false, clipboardwrite: false, reportedat: now },
132
+ }),
133
+ ) as Record<string, unknown>;
134
+ expect(Object.keys(wire).includes("telemetry")).toBe(false);
135
+ expect((wire.session as Record<string, unknown>).jarid).toBe("jar-1");
136
+ expect(outboundcalls).toBe(0);
137
+ });
138
+ });
139
+
140
+ describe("minimization optinsync consent and encryptsync refusal", () => {
141
+ it("stays disabled until the user opts each data class in, records the consent per class and refuses the sync without the passphrase", async () => {
142
+ const offered = ["runs", "memory", "captures", "settings", "provenance"];
143
+ expect(syncgate({ dataclass: "runs" }).allowed).toBe(false);
144
+ const first = optinsync({ offered, enable: ["runs", "memory"], now });
145
+ expect(first.settings.enabled.sort()).toEqual(["memory", "runs"]);
146
+ expect(first.consent.map((entry) => entry.dataclass).sort()).toEqual(["memory", "runs"]);
147
+ expect(first.consent.every((entry) => entry.at === now)).toBe(true);
148
+ expect(syncgate({ settings: first.settings, dataclass: "runs" }).allowed).toBe(true);
149
+ expect(syncgate({ settings: first.settings, dataclass: "captures" }).allowed).toBe(false);
150
+ const second = optinsync({ current: first.settings, offered, enable: ["captures"], now: now + 5000 });
151
+ expect(second.settings.enabled.sort()).toEqual(["captures", "memory", "runs"]);
152
+ expect(second.settings.consent.find((entry) => entry.dataclass === "captures")?.at).toBe(now + 5000);
153
+ expect(second.settings.consent.find((entry) => entry.dataclass === "runs")?.at).toBe(now);
154
+ expect(() => optinsync({ offered, enable: ["telemetry"], now })).toThrow(
155
+ /never enables a class the listing did not offer/,
156
+ );
157
+ expect(() => optinsync({ offered: [], enable: ["runs"], now })).toThrow(/lists every data class/);
158
+ const consentreport = syncconsentreport({ consent: second.settings.consent });
159
+ expect(consentreport.count).toBe(3);
160
+ expect(() =>
161
+ syncconsentreport({
162
+ consent: [
163
+ { dataclass: "runs", at: now },
164
+ { dataclass: "Runs", at: now },
165
+ ],
166
+ }),
167
+ ).toThrow(/one consent stamp/);
168
+ await expect(encryptsync({ payload: '{"runs":[]}', derive, encrypt })).rejects.toThrow(/needs the user passphrase/);
169
+ const encrypted = await encryptsync({
170
+ payload: '{"runs":[]}',
171
+ passphrase: "correct horse battery staple",
172
+ derive,
173
+ encrypt,
174
+ });
175
+ expect(encrypted.cipher).toBe("key-28:11");
176
+ expect(encrypted.formattag).toBe("devthink-sync-1");
177
+ expect(encryptsyncgate({ encrypted: true, formattag: encrypted.formattag }).allowed).toBe(true);
178
+ expect(encryptsyncgate({ encrypted: false, formattag: encrypted.formattag }).allowed).toBe(false);
179
+ expect(encryptsyncgate({ encrypted: true, formattag: " " }).allowed).toBe(false);
180
+ const settings: syncsettings = second.settings;
181
+ await expect(
182
+ syncpass({
183
+ payload: '{"captures":[]}',
184
+ classes: ["settings"],
185
+ settings,
186
+ derive,
187
+ encrypt,
188
+ send: async () => ({ delivered: true }),
189
+ now,
190
+ }),
191
+ ).rejects.toThrow(/never opted in/);
192
+ const delivered = await syncpass({
193
+ payload: '{"runs":[]}',
194
+ classes: ["runs"],
195
+ settings,
196
+ passphrase: "correct horse battery staple",
197
+ derive,
198
+ encrypt,
199
+ send: async () => ({ delivered: true, endpoint: "https://sync.example" }),
200
+ now,
201
+ });
202
+ expect(delivered.delivered).toBe(true);
203
+ expect(delivered.record.encrypted).toBe(true);
204
+ expect(delivered.record.classes).toEqual(["runs"]);
205
+ expect(delivered.record.formattag).toBe("devthink-sync-1");
206
+ const envelope = syncpayloadreport({
207
+ classes: delivered.record.classes,
208
+ payloadhash: delivered.record.payloadhash,
209
+ formattag: delivered.record.formattag,
210
+ encrypted: delivered.record.encrypted,
211
+ syncedat: delivered.record.syncedat,
212
+ });
213
+ expect(envelope.encrypted).toBe(true);
214
+ expect(() =>
215
+ syncpayloadreport({
216
+ classes: ["runs"],
217
+ payloadhash: "h",
218
+ formattag: "devthink-sync-1",
219
+ encrypted: false,
220
+ syncedat: now,
221
+ }),
222
+ ).toThrow(/plaintext/);
223
+ });
224
+ });
225
+
226
+ describe("minimization purgeonrequest scopes and cleanup retention", () => {
227
+ it("deletes the stored keys by scope on the typed confirmation while the audit hashes survive and the retained artifacts stay", () => {
228
+ const inventory: datainventory[] = [
229
+ { key: "runs", dataclass: "runs", size: 120, records: 2, at: now },
230
+ { key: "memoryitems", dataclass: "memory", size: 80, records: 1, at: now },
231
+ { key: "captures", dataclass: "captures", size: 900, records: 3, at: now },
232
+ { key: "settings", dataclass: "settings", size: 40, records: 1, at: now },
233
+ { key: "provlog:run1", dataclass: "provenance", size: 60, records: 2, at: now },
234
+ { key: "audit", dataclass: "audit", size: 300, records: 9, at: now },
235
+ ];
236
+ const policy: purgepolicy = {
237
+ scope: ["runs", "memory", "captures", "settings", "provenance"],
238
+ confirmation: "purge everything",
239
+ at: now - 2000,
240
+ };
241
+ expect(() => purgeonrequest({ policy, typed: "purge everythin", inventory, now })).toThrow(/typed confirmation/);
242
+ expect(
243
+ purgegate({ scope: policy.scope, confirmation: policy.confirmation, typed: "purge everything" }).allowed,
244
+ ).toBe(true);
245
+ expect(purgegate({ scope: policy.scope, confirmation: policy.confirmation, typed: "" }).allowed).toBe(false);
246
+ expect(purgegate({ scope: ["captures"], confirmation: policy.confirmation, typed: "" }).allowed).toBe(true);
247
+ const full = purgeonrequest({ policy, typed: "purge everything", inventory, now });
248
+ expect(full.deleted.sort()).toEqual(["captures", "memoryitems", "provlog:run1", "runs", "settings"]);
249
+ expect(full.kept).toEqual(["audit"]);
250
+ expect(full.audithashespreserved).toBe(true);
251
+ const partial = purgeonrequest({
252
+ policy: { scope: ["captures"], confirmation: "purge everything", at: now },
253
+ typed: "",
254
+ inventory,
255
+ now,
256
+ });
257
+ expect(partial.deleted).toEqual(["captures"]);
258
+ expect(partial.kept.sort()).toEqual(["audit", "memoryitems", "provlog:run1", "runs", "settings"]);
259
+ expect(() =>
260
+ purgeonrequest({ policy: { scope: [], confirmation: "x", at: now }, typed: "", inventory, now }),
261
+ ).toThrow(/names its scope/);
262
+ const artifacts = [
263
+ { id: "exp-1", dataclass: "exports" },
264
+ { id: "exp-2", dataclass: "exports", retained: true },
265
+ { id: "cap-1", dataclass: "captures" },
266
+ { id: "med-1", dataclass: "media" },
267
+ ];
268
+ const pass = cleanupafterrun({ runid: "run1", artifacts, classes: ["exports", "captures"], now });
269
+ expect(pass.cleared.sort()).toEqual(["cap-1", "exp-1"]);
270
+ expect(pass.kept.sort()).toEqual(["exp-2", "med-1"]);
271
+ expect(() => cleanupafterrun({ runid: "run1", artifacts, classes: ["audit"], now })).toThrow(
272
+ /never deletes the audit history/,
273
+ );
274
+ expect(cleanupgate({ touchesaudit: false, consent: false }).allowed).toBe(true);
275
+ expect(cleanupgate({ touchesaudit: true, consent: false }).allowed).toBe(false);
276
+ expect(cleanupgate({ touchesaudit: true, consent: true }).allowed).toBe(true);
277
+ });
278
+ });
279
+
280
+ describe("minimization exportall completeness and streaming", () => {
281
+ it("bundles every stored record into one portable file and streams the chunks without a size cap", () => {
282
+ expect(exportallgate({ useraction: false, records: 4 }).allowed).toBe(false);
283
+ expect(exportallgate({ useraction: true, records: 0 }).allowed).toBe(false);
284
+ expect(exportallgate({ useraction: true, records: 4 }).allowed).toBe(true);
285
+ const bundle = exportall({
286
+ runs: ["run1", "run2"],
287
+ memory: ["mem1"],
288
+ captures: ["cap1", "cap2", "cap3"],
289
+ settings: true,
290
+ provenance: ["prov1", "prov2"],
291
+ now,
292
+ });
293
+ expect(bundle.runs).toEqual(["run1", "run2"]);
294
+ expect(bundle.captures).toHaveLength(3);
295
+ expect(bundle.records).toBe(9);
296
+ expect(bundle.bytes).toBeGreaterThan(0);
297
+ const chunks = streambundle({ bundle, chunksize: 64 });
298
+ expect(chunks.length).toBeGreaterThanOrEqual(Math.ceil(bundle.bytes / 64));
299
+ expect(chunks.join("")).toBe(JSON.stringify(bundle));
300
+ const onegiant = streambundle({ bundle, chunksize: bundle.bytes + 1000 });
301
+ expect(onegiant).toHaveLength(1);
302
+ expect(() => streambundle({ bundle, chunksize: 0 })).toThrow(/positive whole number/);
303
+ expect(() => exportall({ runs: [], memory: [], captures: [], settings: false, provenance: [], now })).toThrow(
304
+ /exports nothing/,
305
+ );
306
+ });
307
+ });
308
+
309
+ describe("minimization cookiejar sealing and enforcequarantine verdicts", () => {
310
+ it("assigns one jar per run, scopes the cookie read and write to the active jar, seals it at the completion and releases or deletes the quarantined files by the verdict alone", () => {
311
+ const jar = newjar({ runid: "run1", now });
312
+ expect(jar.sealed).toBe(false);
313
+ expect(jar.cookies).toEqual([]);
314
+ expect(() => newjar({ runid: " ", now })).toThrow(/names its run/);
315
+ expect(jargate({ jarid: jar.jarid, jar, runid: "run1", operation: "write" }).allowed).toBe(true);
316
+ expect(jargate({ jarid: jar.jarid, jar, runid: "run2", operation: "write" }).allowed).toBe(false);
317
+ const nojar: Parameters<typeof jargate>[0] = { runid: "run1", operation: "read" };
318
+ expect(jargate(nojar).allowed).toBe(false);
319
+ const written = jarscope({
320
+ jar,
321
+ operation: "write",
322
+ cookies: [
323
+ { name: "session", domain: "shop.example", value: "abc" },
324
+ { name: "cart", domain: "shop.example", value: "2" },
325
+ ],
326
+ now: now + 100,
327
+ });
328
+ expect(written.jar.cookies).toHaveLength(2);
329
+ const rewritten = jarscope({
330
+ jar: written.jar,
331
+ operation: "write",
332
+ cookies: [{ name: "cart", domain: "shop.example", value: "3" }],
333
+ now: now + 200,
334
+ });
335
+ expect(rewritten.jar.cookies).toHaveLength(2);
336
+ expect(rewritten.jar.cookies.find((cookie) => cookie.name === "cart")?.value).toBe("3");
337
+ const read = jarscope({ jar: rewritten.jar, operation: "read", domain: "shop.example", now: now + 300 });
338
+ expect(read.entries).toHaveLength(2);
339
+ const otherdomain = jarscope({ jar: rewritten.jar, operation: "read", domain: "other.example", now: now + 300 });
340
+ expect(otherdomain.entries).toHaveLength(0);
341
+ expect(() => jarscope({ jar: rewritten.jar, operation: "write", cookies: [], now })).toThrow(/empty write/);
342
+ const sealed = sealjar({ jar: rewritten.jar, now: now + 400, expiry: 60000 });
343
+ expect(sealed.sealed).toBe(true);
344
+ expect(sealed.sealedat).toBe(now + 400);
345
+ expect(sealed.expiresat).toBe(now + 60400);
346
+ expect(jargate({ jarid: sealed.jarid, jar: sealed, runid: "run1", operation: "write" }).allowed).toBe(false);
347
+ expect(jargate({ jarid: sealed.jarid, jar: sealed, runid: "run1", operation: "read" }).allowed).toBe(true);
348
+ expect(() =>
349
+ jarscope({
350
+ jar: sealed,
351
+ operation: "write",
352
+ cookies: [{ name: "late", domain: "shop.example", value: "x" }],
353
+ now: now + 500,
354
+ }),
355
+ ).toThrow(/sealed jar refuses every write/);
356
+ expect(() => sealjar({ jar: rewritten.jar, now, expiry: 0 })).toThrow(/positive number of milliseconds/);
357
+ expect(sealjar({ jar: sealed, now: now + 600 }).sealedat).toBe(now + 400);
358
+ const held = enforcequarantine({
359
+ path: "/sandbox/invoice.pdf",
360
+ reason: "download moved out of the downloads folder",
361
+ now,
362
+ });
363
+ expect(held.entry.scan).toBe("pending");
364
+ expect(held.entry.status).toBe("held");
365
+ expect(held.action).toBe("hold");
366
+ const clean = enforcequarantine({
367
+ path: "/sandbox/invoice.pdf",
368
+ reason: "download moved out of the downloads folder",
369
+ scan: "clean",
370
+ now,
371
+ });
372
+ expect(clean.action).toBe("release");
373
+ expect(clean.entry.status).toBe("released");
374
+ const flagged = enforcequarantine({
375
+ path: "/sandbox/invoice.pdf",
376
+ reason: "download moved out of the downloads folder",
377
+ scan: "flagged",
378
+ now,
379
+ });
380
+ expect(flagged.action).toBe("delete");
381
+ expect(flagged.entry.status).toBe("deleted");
382
+ const errored = enforcequarantine({ path: "/sandbox/invoice.pdf", reason: "download moved", scan: "error", now });
383
+ expect(errored.action).toBe("hold");
384
+ expect(() => enforcequarantine({ path: " ", reason: "x", now })).toThrow(/names its file path/);
385
+ expect(quarantineopengate({ verdict: "clean" }).allowed).toBe(true);
386
+ expect(quarantineopengate({ verdict: "flagged" }).allowed).toBe(false);
387
+ expect(quarantineopengate({ verdict: "pending" }).allowed).toBe(false);
388
+ const verdictreport = quarantineverdictreport({
389
+ entry: {
390
+ id: clean.entry.id,
391
+ path: clean.entry.path,
392
+ reason: clean.entry.reason,
393
+ scan: clean.entry.scan,
394
+ status: clean.entry.status ?? "held",
395
+ at: clean.entry.at,
396
+ updatedat: clean.entry.updatedat,
397
+ },
398
+ });
399
+ expect(verdictreport.released).toBe(true);
400
+ expect(() =>
401
+ quarantineverdictreport({
402
+ entry: {
403
+ id: "q1",
404
+ path: "/sandbox/x",
405
+ reason: "r",
406
+ scan: "flagged",
407
+ status: "released",
408
+ at: now,
409
+ updatedat: now,
410
+ },
411
+ }),
412
+ ).toThrow(/Only a clean scanner verdict releases/);
413
+ });
414
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { bumprevision, fallbackroute, markprovider, resolveroute, routevalid, routesfor } from "../llm.js";
3
+ import type { modelroute, providerconfig } from "../types.js";
4
+
5
+ const now = 1_800_000_000_000;
6
+
7
+ /** Builds one provider config fixture with every value user configured. */
8
+ function provider(id: string, over: Partial<providerconfig> = {}): providerconfig {
9
+ return {
10
+ id,
11
+ name: `The ${id} gateway`,
12
+ endpoint: `https://${id}.example/v1`,
13
+ style: "chatcompletions",
14
+ models: ["model-a", "model-b"],
15
+ status: "available",
16
+ createdat: now,
17
+ ...over,
18
+ };
19
+ }
20
+
21
+ /** Builds one model route fixture. */
22
+ function route(over: Partial<modelroute> = {}): modelroute {
23
+ return {
24
+ id: "route1",
25
+ kind: "draftplan",
26
+ providerid: "prov1",
27
+ model: "model-a",
28
+ revision: 1,
29
+ updatedat: now,
30
+ ...over,
31
+ };
32
+ }
33
+
34
+ describe("modelroute", () => {
35
+ it("resolves the provider and model for a task kind", () => {
36
+ const resolved = resolveroute({ routes: [route()], providers: [provider("prov1")], kind: "draftplan" });
37
+ expect(resolved.provider?.id).toBe("prov1");
38
+ expect(resolved.model).toBe("model-a");
39
+ });
40
+
41
+ it("reports why nothing routed for an unrouted task kind", () => {
42
+ const resolved = resolveroute({ routes: [route()], providers: [provider("prov1")], kind: "parsecommand" });
43
+ expect(resolved.provider).toBeUndefined();
44
+ expect(resolved.reason).toMatch(/No model route/i);
45
+ });
46
+
47
+ it("keeps every route of one task kind in revision order", () => {
48
+ const routes = [
49
+ route({ revision: 1 }),
50
+ route({ id: "route2", revision: 5 }),
51
+ route({ id: "route3", kind: "reflect" }),
52
+ ];
53
+ expect(routesfor(routes, "draftplan").map((entry) => entry.id)).toEqual(["route2", "route1"]);
54
+ });
55
+
56
+ it("refuses a route whose model stays outside the provider model list", () => {
57
+ const resolved = resolveroute({
58
+ routes: [route({ model: "model-z" })],
59
+ providers: [provider("prov1")],
60
+ kind: "draftplan",
61
+ });
62
+ expect(resolved.reason).toMatch(/outside the model list/i);
63
+ });
64
+
65
+ it("marks providers unavailable and available again after their calls", () => {
66
+ const providers = markprovider({
67
+ providers: [provider("prov1"), provider("prov2")],
68
+ providerid: "prov1",
69
+ available: false,
70
+ now,
71
+ });
72
+ expect(providers.find((candidate) => candidate.id === "prov1")?.status).toBe("unavailable");
73
+ expect(providers.find((candidate) => candidate.id === "prov2")?.status).toBe("available");
74
+ const resolved = resolveroute({ routes: [route()], providers, kind: "draftplan" });
75
+ expect(resolved.reason).toMatch(/unavailable/i);
76
+ const back = markprovider({ providers, providerid: "prov1", available: true, now: now + 1000 });
77
+ expect(resolveroute({ routes: [route()], providers: back, kind: "draftplan" }).provider?.id).toBe("prov1");
78
+ expect(back.find((candidate) => candidate.id === "prov1")?.lastcheckedat).toBe(now + 1000);
79
+ });
80
+
81
+ it("falls back on the user configured fallback pair when the primary provider fails", () => {
82
+ const routes = [route({ fallbackproviderid: "prov2", fallbackmodel: "model-b" })];
83
+ const providers = markprovider({
84
+ providers: [provider("prov1"), provider("prov2")],
85
+ providerid: "prov1",
86
+ available: false,
87
+ now,
88
+ });
89
+ const fallen = fallbackroute({ routes, providers, kind: "draftplan" });
90
+ expect(fallen.provider?.id).toBe("prov2");
91
+ expect(fallen.model).toBe("model-b");
92
+ });
93
+
94
+ it("reports why nothing fell back without a fallback pair or with an unavailable fallback provider", () => {
95
+ expect(fallbackroute({ routes: [route()], providers: [provider("prov1")], kind: "draftplan" }).reason).toMatch(
96
+ /no user configured fallback/i,
97
+ );
98
+ const routes = [route({ fallbackproviderid: "prov2", fallbackmodel: "model-b" })];
99
+ const providers = [provider("prov1"), provider("prov2", { status: "unavailable" })];
100
+ expect(fallbackroute({ routes, providers, kind: "draftplan" }).reason).toMatch(/unavailable/i);
101
+ });
102
+
103
+ it("validates route entries and bumps revisions for the history", () => {
104
+ expect(routevalid(route()).allowed).toBe(true);
105
+ expect(routevalid(route({ kind: " " })).allowed).toBe(false);
106
+ expect(routevalid(route({ model: "" })).allowed).toBe(false);
107
+ expect(routevalid(route({ fallbackproviderid: "prov2" })).allowed).toBe(false);
108
+ const bumped = bumprevision(route(), now + 5000);
109
+ expect(bumped.revision).toBe(2);
110
+ expect(bumped.updatedat).toBe(now + 5000);
111
+ });
112
+ });