@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,696 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ addusage,
4
+ budgetcheck,
5
+ buildrequest,
6
+ calllocal,
7
+ callmodel,
8
+ classifyintent,
9
+ draftplan,
10
+ guardoutput,
11
+ islocalorigin,
12
+ parsecommand,
13
+ parsecompletion,
14
+ parseoutput,
15
+ parsestream,
16
+ reflectionsummary,
17
+ reflectstep,
18
+ replannonfail,
19
+ streammodel,
20
+ stripguardrails,
21
+ toolbriefof,
22
+ rendertoolbriefs,
23
+ usagetotals,
24
+ } from "../llm.js";
25
+ import { buildtoolcatalog } from "../tools.js";
26
+ import type { modelmessage, parseguard, plandraft, providerconfig, usagerecord } from "../types.js";
27
+ import type { fetchtransport, transportresponse } from "../http.js";
28
+
29
+ const now = 1_800_000_000_000;
30
+ const instant = (): Promise<void> => Promise.resolve();
31
+
32
+ /** Builds one provider config fixture with every value user configured. */
33
+ function provider(over: Partial<providerconfig> = {}): providerconfig {
34
+ return {
35
+ id: "prov1",
36
+ name: "The gateway",
37
+ endpoint: "https://gateway.example/v1",
38
+ style: "chatcompletions",
39
+ models: ["model-a", "model-b"],
40
+ status: "available",
41
+ createdat: now,
42
+ ...over,
43
+ };
44
+ }
45
+
46
+ /** Builds one stub transport that answers every request with the canned body. */
47
+ function stub(body: string, status = 200, seen: string[] = []): fetchtransport {
48
+ return async (url, init): Promise<transportresponse> => {
49
+ seen.push(`${init.method} ${url} ${init.body ?? ""}`);
50
+ return { status, headers: { "content-type": "application/json" }, body };
51
+ };
52
+ }
53
+
54
+ /** Wraps one model answer payload in the chat completions envelope the style parse expects. */
55
+ function answer(payload: string): string {
56
+ return JSON.stringify({ choices: [{ message: { role: "assistant", content: payload } }] });
57
+ }
58
+
59
+ /** The chat completions answer body fixture. */
60
+ const chatbody = JSON.stringify({
61
+ choices: [{ message: { role: "assistant", content: "the answer" } }],
62
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
63
+ });
64
+
65
+ describe("llm request shaping", () => {
66
+ it("shapes the chat completions request with the bearer header and the message envelope", () => {
67
+ const shaped = buildrequest({
68
+ provider: provider(),
69
+ model: "model-a",
70
+ messages: [
71
+ { role: "system", content: "be brief" },
72
+ { role: "user", content: "hello" },
73
+ ],
74
+ apikey: "secret",
75
+ temperature: 0.2,
76
+ maxtokens: 128,
77
+ });
78
+ expect(shaped.url).toBe("https://gateway.example/v1");
79
+ expect(shaped.method).toBe("POST");
80
+ expect(shaped.headers.authorization).toBe("Bearer secret");
81
+ const body = JSON.parse(shaped.body) as Record<string, unknown>;
82
+ expect(body.model).toBe("model-a");
83
+ expect(body.temperature).toBe(0.2);
84
+ expect(body.max_tokens).toBe(128);
85
+ expect(body.messages).toEqual([
86
+ { role: "system", content: "be brief" },
87
+ { role: "user", content: "hello" },
88
+ ]);
89
+ });
90
+
91
+ it("shapes the responses request with the instructions block and the turns", () => {
92
+ const shaped = buildrequest({
93
+ provider: provider({ style: "responses" }),
94
+ model: "model-a",
95
+ messages: [
96
+ { role: "system", content: "be brief" },
97
+ { role: "user", content: "hello" },
98
+ { role: "assistant", content: "hi" },
99
+ ],
100
+ apikey: "secret",
101
+ maxtokens: 64,
102
+ });
103
+ const body = JSON.parse(shaped.body) as Record<string, unknown>;
104
+ expect(body.instructions).toBe("be brief");
105
+ expect(body.max_output_tokens).toBe(64);
106
+ expect(body.input).toEqual([
107
+ { role: "user", content: "hello" },
108
+ { role: "assistant", content: "hi" },
109
+ ]);
110
+ expect(shaped.headers.authorization).toBe("Bearer secret");
111
+ });
112
+
113
+ it("shapes the messages request with the api key header and the system block", () => {
114
+ const shaped = buildrequest({
115
+ provider: provider({ style: "messages" }),
116
+ model: "model-a",
117
+ messages: [
118
+ { role: "system", content: "be brief" },
119
+ { role: "user", content: "hello" },
120
+ ],
121
+ apikey: "secret",
122
+ temperature: 1,
123
+ });
124
+ expect(shaped.headers["x-api-key"]).toBe("secret");
125
+ const body = JSON.parse(shaped.body) as Record<string, unknown>;
126
+ expect(body.system).toBe("be brief");
127
+ expect(body.messages).toEqual([{ role: "user", content: "hello" }]);
128
+ expect(body.temperature).toBe(1);
129
+ });
130
+
131
+ it("shapes the gemini request with the key in the url and the contents envelope", () => {
132
+ const shaped = buildrequest({
133
+ provider: provider({ endpoint: "https://gateway.example/v1/models/x:generateContent", style: "gemini" }),
134
+ model: "model-a",
135
+ messages: [
136
+ { role: "system", content: "be brief" },
137
+ { role: "user", content: "hello" },
138
+ ],
139
+ apikey: "secret",
140
+ maxtokens: 32,
141
+ });
142
+ expect(shaped.url).toBe("https://gateway.example/v1/models/x:generateContent?key=secret");
143
+ const body = JSON.parse(shaped.body) as Record<string, unknown>;
144
+ expect(body.systemInstruction).toEqual({ parts: [{ text: "be brief" }] });
145
+ expect(body.contents).toEqual([{ role: "user", parts: [{ text: "hello" }] }]);
146
+ expect((body.generationConfig as Record<string, unknown>).maxOutputTokens).toBe(32);
147
+ });
148
+
149
+ it("merges the user configured headers over the protocol shape headers", () => {
150
+ const shaped = buildrequest({
151
+ provider: provider({ headers: { "x-gateway": "one", authorization: "Bearer user" } }),
152
+ model: "model-a",
153
+ messages: [{ role: "user", content: "hi" }],
154
+ apikey: "secret",
155
+ });
156
+ expect(shaped.headers["x-gateway"]).toBe("one");
157
+ expect(shaped.headers.authorization).toBe("Bearer user");
158
+ });
159
+ });
160
+
161
+ describe("llm response parsing", () => {
162
+ it("parses the chat completions answer with its usage", () => {
163
+ const parsed = parsecompletion("chatcompletions", chatbody);
164
+ expect(parsed.text).toBe("the answer");
165
+ expect(parsed.usage).toEqual({ prompttokens: 10, completiontokens: 5, totaltokens: 15 });
166
+ });
167
+
168
+ it("parses the responses answer text and usage", () => {
169
+ const body = JSON.stringify({
170
+ output: [
171
+ {
172
+ content: [
173
+ { type: "output_text", text: "part one " },
174
+ { type: "output_text", text: "part two" },
175
+ ],
176
+ },
177
+ ],
178
+ usage: { input_tokens: 7, output_tokens: 3, total_tokens: 10 },
179
+ });
180
+ const parsed = parsecompletion("responses", body);
181
+ expect(parsed.text).toBe("part one part two");
182
+ expect(parsed.usage?.totaltokens).toBe(10);
183
+ });
184
+
185
+ it("parses the messages answer text blocks with the token counts", () => {
186
+ const body = JSON.stringify({
187
+ content: [
188
+ { type: "text", text: "hello " },
189
+ { type: "text", text: "there" },
190
+ ],
191
+ usage: { input_tokens: 4, output_tokens: 2 },
192
+ });
193
+ const parsed = parsecompletion("messages", body);
194
+ expect(parsed.text).toBe("hello there");
195
+ expect(parsed.usage).toEqual({ prompttokens: 4, completiontokens: 2, totaltokens: 6 });
196
+ });
197
+
198
+ it("parses the gemini answer candidate with its usage metadata", () => {
199
+ const body = JSON.stringify({
200
+ candidates: [{ content: { parts: [{ text: "gem answer" }] } }],
201
+ usageMetadata: { promptTokenCount: 9, candidatesTokenCount: 2, totalTokenCount: 11 },
202
+ });
203
+ const parsed = parsecompletion("gemini", body);
204
+ expect(parsed.text).toBe("gem answer");
205
+ expect(parsed.usage?.totaltokens).toBe(11);
206
+ });
207
+
208
+ it("refuses malformed answers with the reason in plain language", () => {
209
+ expect(parsecompletion("chatcompletions", "not json").reason).toMatch(/not json/i);
210
+ expect(parsecompletion("chatcompletions", JSON.stringify({ choices: [] })).reason).toMatch(/no message content/i);
211
+ expect(parsecompletion("responses", JSON.stringify({ output: [] })).reason).toMatch(/no output text/i);
212
+ expect(parsecompletion("messages", JSON.stringify({ content: [] })).reason).toMatch(/no text block/i);
213
+ expect(parsecompletion("gemini", JSON.stringify({ candidates: [] })).reason).toMatch(/no candidate text/i);
214
+ });
215
+ });
216
+
217
+ describe("llm provider calls", () => {
218
+ it("posts one completion through the fetch machinery and returns the parsed text and usage", async () => {
219
+ const seen: string[] = [];
220
+ const outcome = await callmodel({
221
+ provider: provider(),
222
+ model: "model-a",
223
+ messages: [{ role: "user", content: "hello" }],
224
+ apikey: "secret",
225
+ transport: stub(chatbody, 200, seen),
226
+ sleep: instant,
227
+ now: () => now,
228
+ });
229
+ expect(outcome.text).toBe("the answer");
230
+ expect(outcome.usage?.totaltokens).toBe(15);
231
+ expect(seen[0]).toContain("POST https://gateway.example/v1");
232
+ expect(JSON.parse((seen[0] ?? "").split(" ").slice(2).join(" ") || "{}")).toMatchObject({ model: "model-a" });
233
+ });
234
+
235
+ it("refuses the call when the configured auth reference has no resolved key", async () => {
236
+ const authref = {
237
+ name: "gateway key",
238
+ origins: ["https://gateway.example"],
239
+ header: "authorization",
240
+ storageid: "key-store-1",
241
+ configuredat: now,
242
+ };
243
+ await expect(
244
+ callmodel({
245
+ provider: provider({ authref }),
246
+ model: "model-a",
247
+ messages: [{ role: "user", content: "hi" }],
248
+ transport: stub(chatbody),
249
+ sleep: instant,
250
+ now: () => now,
251
+ }),
252
+ ).rejects.toThrow(/resolved key/i);
253
+ });
254
+
255
+ it("refuses the call when page content the user has not granted would leave", async () => {
256
+ await expect(
257
+ callmodel({
258
+ provider: provider(),
259
+ model: "model-a",
260
+ messages: [{ role: "user", content: "summarize" }],
261
+ pagecontent: "the page text",
262
+ pagegrant: false,
263
+ transport: stub(chatbody),
264
+ sleep: instant,
265
+ now: () => now,
266
+ }),
267
+ ).rejects.toThrow(/has not granted/i);
268
+ const granted = await callmodel({
269
+ provider: provider(),
270
+ model: "model-a",
271
+ messages: [{ role: "user", content: "summarize" }],
272
+ pagecontent: "the page text",
273
+ pagegrant: true,
274
+ transport: stub(chatbody),
275
+ sleep: instant,
276
+ now: () => now,
277
+ });
278
+ expect(granted.text).toBe("the answer");
279
+ });
280
+
281
+ it("refuses the call when the provider carries no user configured endpoint", async () => {
282
+ await expect(
283
+ callmodel({
284
+ provider: provider({ endpoint: " " }),
285
+ model: "model-a",
286
+ messages: [{ role: "user", content: "hi" }],
287
+ transport: stub(chatbody),
288
+ sleep: instant,
289
+ now: () => now,
290
+ }),
291
+ ).rejects.toThrow(/endpoint/i);
292
+ });
293
+
294
+ it("refuses the call when the answer stays malformed after the retries", async () => {
295
+ await expect(
296
+ callmodel({
297
+ provider: provider(),
298
+ model: "model-a",
299
+ messages: [{ role: "user", content: "hi" }],
300
+ apikey: "k",
301
+ transport: stub("<html>bad gateway</html>"),
302
+ sleep: instant,
303
+ options: { retries: 1, backoff: 0 },
304
+ now: () => now,
305
+ }),
306
+ ).rejects.toThrow(/not json/i);
307
+ });
308
+ });
309
+
310
+ describe("llm local model calls", () => {
311
+ it("posts to the local model endpoint with no external network", async () => {
312
+ const seen: string[] = [];
313
+ const outcome = await calllocal({
314
+ local: { endpoint: "http://127.0.0.1:8080/v1", model: "local-model", style: "chatcompletions" },
315
+ messages: [{ role: "user", content: "hi" }],
316
+ transport: stub(chatbody, 200, seen),
317
+ sleep: instant,
318
+ now: () => now,
319
+ });
320
+ expect(outcome.text).toBe("the answer");
321
+ expect(seen[0]).toContain("http://127.0.0.1:8080/v1");
322
+ });
323
+
324
+ it("refuses a local model endpoint outside the machine", async () => {
325
+ await expect(
326
+ calllocal({
327
+ local: { endpoint: "https://remote.example/v1", model: "m", style: "chatcompletions" },
328
+ messages: [{ role: "user", content: "hi" }],
329
+ transport: stub(chatbody),
330
+ sleep: instant,
331
+ now: () => now,
332
+ }),
333
+ ).rejects.toThrow(/never leaves the machine/i);
334
+ expect(islocalorigin("http://localhost:1234/x")).toBe(true);
335
+ expect(islocalorigin("https://gateway.example/v1")).toBe(false);
336
+ });
337
+ });
338
+
339
+ describe("llm streaming", () => {
340
+ it("parses the ordered token stream of every protocol style", () => {
341
+ const chat = parsestream(
342
+ "chatcompletions",
343
+ 'data: {"choices":[{"delta":{"content":"he"}}]}\n\ndata: {"choices":[{"delta":{"content":"llo"}}]}\n\ndata: [DONE]',
344
+ );
345
+ expect(chat.map((token) => token.text).join("")).toBe("hello");
346
+ expect(chat[1]?.done).toBe(true);
347
+ const messages = parsestream(
348
+ "messages",
349
+ 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"a"}}\ndata: {"type":"content_block_delta","delta":{"type":"text_delta","text":"b"}}',
350
+ );
351
+ expect(messages.map((token) => token.text).join("")).toBe("ab");
352
+ const responses = parsestream(
353
+ "responses",
354
+ 'data: {"type":"response.output_text.delta","delta":"x"}\ndata: {"type":"response.completed"}',
355
+ );
356
+ expect(responses.map((token) => token.text).join("")).toBe("x");
357
+ const gemini = parsestream("gemini", 'data: {"candidates":[{"content":{"parts":[{"text":"g"}]}}]}');
358
+ expect(gemini.map((token) => token.text).join("")).toBe("g");
359
+ });
360
+
361
+ it("streams one completion when the provider supports it and assembles the answer", async () => {
362
+ const body =
363
+ 'data: {"choices":[{"delta":{"content":"to"}}]}\ndata: {"choices":[{"delta":{"content":"ken"}}]}\ndata: [DONE]';
364
+ const outcome = await streammodel({
365
+ provider: provider(),
366
+ model: "model-a",
367
+ messages: [{ role: "user", content: "hi" }],
368
+ apikey: "k",
369
+ transport: stub(body),
370
+ sleep: instant,
371
+ now: () => now,
372
+ });
373
+ expect(outcome.text).toBe("token");
374
+ expect(outcome.tokens).toBe(2);
375
+ });
376
+ });
377
+
378
+ describe("llm command parsing and intents", () => {
379
+ const parsebody = JSON.stringify({
380
+ intent: "navigate",
381
+ entities: [{ name: "url", value: "https://example.com" }],
382
+ confidence: 0.9,
383
+ });
384
+
385
+ it("parses a natural language command across the intent kinds", async () => {
386
+ for (const intent of ["navigate", "extract", "fill", "monitor", "automate", "ask"] as const) {
387
+ const body = answer(JSON.stringify({ intent, entities: [], confidence: 0.8 }));
388
+ const outcome = await parsecommand({
389
+ provider: provider(),
390
+ model: "model-a",
391
+ text: "do the thing",
392
+ transport: stub(body),
393
+ sleep: instant,
394
+ now: () => now,
395
+ });
396
+ expect(outcome.parse?.intent).toBe(intent);
397
+ expect(outcome.parse?.confidence).toBe(0.8);
398
+ expect(outcome.parse?.model).toBe("model-a");
399
+ }
400
+ });
401
+
402
+ it("carries the extracted entities of the parsed command", async () => {
403
+ const outcome = await parsecommand({
404
+ provider: provider(),
405
+ model: "model-a",
406
+ text: "open the url",
407
+ transport: stub(answer(parsebody)),
408
+ sleep: instant,
409
+ now: () => now,
410
+ });
411
+ expect(outcome.parse?.entities).toEqual([{ name: "url", value: "https://example.com" }]);
412
+ expect(outcome.parse?.providerid).toBe("prov1");
413
+ });
414
+
415
+ it("refuses a model answer whose intent stays outside the intent kinds", async () => {
416
+ const outcome = await parsecommand({
417
+ provider: provider(),
418
+ model: "model-a",
419
+ text: "do the thing",
420
+ transport: stub(answer(JSON.stringify({ intent: "vibes", entities: [], confidence: 1 }))),
421
+ sleep: instant,
422
+ now: () => now,
423
+ });
424
+ expect(outcome.parse).toBeUndefined();
425
+ expect(outcome.reason).toMatch(/intent kinds/i);
426
+ });
427
+
428
+ it("classifies the intent deterministically with confidence thresholds and no provider", () => {
429
+ const navigate = classifyintent("go to the site and open the page");
430
+ expect(navigate.intent).toBe("navigate");
431
+ expect(navigate.confidence).toBeGreaterThan(0.3);
432
+ const extract = classifyintent("extract the table data and read the text");
433
+ expect(extract.intent).toBe("extract");
434
+ const ask = classifyintent("what is this page about");
435
+ expect(ask.intent).toBe("ask");
436
+ const unknown = classifyintent("zzz qqq vvv");
437
+ expect(unknown.intent).toBe("ask");
438
+ expect(unknown.confidence).toBeLessThanOrEqual(0.2);
439
+ });
440
+ });
441
+
442
+ describe("llm plan drafting and replanning", () => {
443
+ const draftbody = JSON.stringify({
444
+ goal: "Read the docs page",
445
+ steps: [
446
+ { kind: "navigate", value: "https://docs.example", summary: "Open the docs page." },
447
+ { kind: "readtext", target: "main", summary: "Read the main text." },
448
+ ],
449
+ openquestions: ["which section first?"],
450
+ });
451
+
452
+ it("drafts a plan from a goal and lints it against the action grammar", async () => {
453
+ const outcome = await draftplan({
454
+ provider: provider(),
455
+ model: "model-a",
456
+ goal: "Read the docs page",
457
+ origin: "https://docs.example",
458
+ transport: stub(answer(draftbody)),
459
+ sleep: instant,
460
+ now: () => now,
461
+ });
462
+ expect(outcome.draft?.steps).toHaveLength(2);
463
+ expect(outcome.draft?.openquestions).toEqual(["which section first?"]);
464
+ expect(outcome.draft?.lintfindings).toEqual([]);
465
+ expect(outcome.draft?.state).toBe("draft");
466
+ });
467
+
468
+ it("records the grammar violations of a model draft before review", async () => {
469
+ const body = JSON.stringify({
470
+ goal: "Do things",
471
+ steps: [
472
+ { kind: "teleport", summary: "Teleport." },
473
+ { kind: "click", summary: "Click nothing." },
474
+ ],
475
+ openquestions: [],
476
+ });
477
+ const outcome = await draftplan({
478
+ provider: provider(),
479
+ model: "model-a",
480
+ goal: "Do things",
481
+ transport: stub(answer(body)),
482
+ sleep: instant,
483
+ now: () => now,
484
+ });
485
+ expect(outcome.draft?.lintfindings.length).toBeGreaterThan(0);
486
+ expect(outcome.draft?.lintfindings.join(" ")).toMatch(/grammar/);
487
+ });
488
+
489
+ it("replans the failed tail while the completed steps stay and the revised steps need fresh review", async () => {
490
+ const draft = outcomeof(draftbody);
491
+ const tailbody = JSON.stringify({ steps: [{ kind: "reload", summary: "Reload and retry." }] });
492
+ const outcome = await replannonfail({
493
+ provider: provider(),
494
+ model: "model-a",
495
+ draft,
496
+ completedstepids: ["step1"],
497
+ failedstepids: ["step2"],
498
+ reason: "The text read timed out.",
499
+ transport: stub(answer(tailbody)),
500
+ sleep: instant,
501
+ now: () => now,
502
+ });
503
+ expect(outcome.replan?.completedstepids).toEqual(["step1"]);
504
+ expect(outcome.replan?.failedstepids).toEqual(["step2"]);
505
+ expect(outcome.replan?.tail).toHaveLength(1);
506
+ expect(outcome.replan?.tail[0]?.freshreview).toBe(true);
507
+ expect(outcome.replan?.state).toBe("pending");
508
+ expect(outcome.replan?.reason).toMatch(/timed out/i);
509
+ });
510
+
511
+ it("reflects one executed step with the running lessons feeding the next prompt", async () => {
512
+ const body = JSON.stringify({
513
+ outcome: "The click landed.",
514
+ lesson: "The button needs a settle window.",
515
+ advice: "Wait one second before the next click.",
516
+ });
517
+ const outcome = await reflectstep({
518
+ provider: provider(),
519
+ model: "model-a",
520
+ runid: "run1",
521
+ stepid: "step2",
522
+ outcome: "The click landed.",
523
+ lessons: ["The page loads slowly."],
524
+ transport: stub(answer(body)),
525
+ sleep: instant,
526
+ now: () => now,
527
+ });
528
+ expect(outcome.note?.lesson).toBe("The button needs a settle window.");
529
+ expect(outcome.note?.advice).toMatch(/one second/);
530
+ const summary = reflectionsummary([outcome.note!]);
531
+ expect(summary).toContain("settle window");
532
+ });
533
+ });
534
+
535
+ describe("llm guardrails", () => {
536
+ const guard: parseguard = {
537
+ schema: { intent: { type: "string", required: true }, confidence: { type: "number" } },
538
+ retries: 2,
539
+ };
540
+
541
+ it("strips code fences and chatter before parsing", () => {
542
+ expect(stripguardrails('Sure! Here you go:\n```json\n{"intent":"navigate"}\n```\nHope that helps.')).toBe(
543
+ '{"intent":"navigate"}',
544
+ );
545
+ expect(stripguardrails('pre chatter {"a":1} post chatter')).toBe('{"a":1}');
546
+ });
547
+
548
+ it("validates model text against the expected schema", () => {
549
+ const valid = parseoutput({ guard, text: '{"intent":"navigate","confidence":0.9}' });
550
+ expect(valid.verdict).toBe("valid");
551
+ expect(valid.parsed?.intent).toBe("navigate");
552
+ const wrongtype = parseoutput({ guard, text: '{"intent":5}' });
553
+ expect(wrongtype.verdict).toBe("invalid");
554
+ expect(wrongtype.reason).toMatch(/asks a string/i);
555
+ });
556
+
557
+ it("retries malformed output up to the configured attempts", () => {
558
+ const outcome = guardoutput({
559
+ guard,
560
+ attempts: ["nope", '{"intent":"fill"}', '{"intent":"fill","confidence":0.5}'],
561
+ });
562
+ expect(outcome.verdict).toBe("valid");
563
+ expect(outcome.attempts).toBe(2);
564
+ });
565
+
566
+ it("refuses the output after the retry exhaustion so nothing executes", () => {
567
+ const outcome = guardoutput({ guard, attempts: ["nope", "still nope", '{"intent":9}'] });
568
+ expect(outcome.verdict).toBe("invalid");
569
+ expect(outcome.attempts).toBe(3);
570
+ expect(outcome.reason).toMatch(/refuses the output/i);
571
+ });
572
+
573
+ it("refuses on the refusal markers without burning retries", () => {
574
+ const outcome = guardoutput({ guard, attempts: ["I cannot comply with that request.", '{"intent":"fill"}'] });
575
+ expect(outcome.verdict).toBe("refused");
576
+ expect(outcome.attempts).toBe(1);
577
+ expect(outcome.reason).toMatch(/refusal marker/i);
578
+ });
579
+
580
+ it("honors the user configured refusal markers over the defaults", () => {
581
+ const outcome = parseoutput({
582
+ guard: { ...guard, refusalmarkers: ["nope-city"] },
583
+ text: '{"intent":"navigate","confidence":0.9}',
584
+ });
585
+ expect(outcome.verdict).toBe("valid");
586
+ const custom = parseoutput({
587
+ guard: { ...guard, refusalmarkers: ["nope-city"] },
588
+ text: "nope-city, the model stays out",
589
+ });
590
+ expect(custom.verdict).toBe("refused");
591
+ expect(custom.reason).toMatch(/nope-city/i);
592
+ });
593
+ });
594
+
595
+ describe("llm tool briefs", () => {
596
+ it("renders one tool brief in openapi style with typed parameters", () => {
597
+ const tool = buildtoolcatalog()
598
+ .domains.flatMap((domain) => domain.tools)
599
+ .find((candidate) => candidate.name === "browser.readtext");
600
+ expect(tool).toBeDefined();
601
+ const brief = toolbriefof(tool!);
602
+ expect(brief.tool).toBe("browser.readtext");
603
+ expect(brief.parameters.some((parameter) => parameter.name === "target" && parameter.required)).toBe(true);
604
+ });
605
+
606
+ it("renders the tool briefs with the consent notice for model consumption", () => {
607
+ const tools = buildtoolcatalog()
608
+ .domains.flatMap((domain) => domain.tools)
609
+ .slice(0, 3);
610
+ const rendered = rendertoolbriefs(tools);
611
+ expect(rendered).toContain("tools:");
612
+ expect(rendered).toContain("- tool: ");
613
+ expect(rendered).toContain("consent: every tool with side effects executes only the approved plan step it names");
614
+ });
615
+ });
616
+
617
+ describe("llm usage and budgets", () => {
618
+ /** Builds one usage record fixture. */
619
+ function usage(over: Partial<usagerecord> = {}): usagerecord {
620
+ return {
621
+ id: "u1",
622
+ runid: "run1",
623
+ providerid: "prov1",
624
+ endpoint: "https://gateway.example/v1",
625
+ model: "model-a",
626
+ prompttokens: 10,
627
+ completiontokens: 5,
628
+ totaltokens: 15,
629
+ cost: 0.01,
630
+ at: now,
631
+ ...over,
632
+ };
633
+ }
634
+
635
+ it("accumulates the usage per run and per step", () => {
636
+ const records = addusage(
637
+ addusage(addusage([], usage()), usage({ id: "u2", stepid: "s1", totaltokens: 20, cost: 0.02 })),
638
+ usage({ id: "u3", runid: "run2", totaltokens: 100, cost: 0.5 }),
639
+ );
640
+ expect(records[0]?.id).toBe("u3");
641
+ expect(usagetotals(records)).toEqual({
642
+ prompttokens: 30,
643
+ completiontokens: 15,
644
+ totaltokens: 135,
645
+ cost: 0.53,
646
+ calls: 3,
647
+ });
648
+ expect(usagetotals(records, { runid: "run1" }).calls).toBe(2);
649
+ expect(usagetotals(records, { runid: "run1", stepid: "s1" }).totaltokens).toBe(20);
650
+ expect(usagetotals(records, { since: now + 1 }).calls).toBe(0);
651
+ });
652
+
653
+ it("halts the run and asks the user when a ceiling is reached", () => {
654
+ const budget = { maxtokens: 100, maxcost: 1, currency: "usd", configuredat: now };
655
+ expect(budgetcheck({ budget, totals: { totaltokens: 99, cost: 0.2 } }).allowed).toBe(true);
656
+ const halted = budgetcheck({ budget, totals: { totaltokens: 100, cost: 0.2 } });
657
+ expect(halted.halted).toBe(true);
658
+ expect(halted.asksuser).toBe(true);
659
+ expect(halted.reason).toMatch(/token ceiling/i);
660
+ const costly = budgetcheck({ budget, totals: { totaltokens: 50, cost: 1 } });
661
+ expect(costly.halted).toBe(true);
662
+ expect(costly.reason).toMatch(/cost ceiling/i);
663
+ });
664
+
665
+ it("keeps an absent budget unbounded with no silent default", () => {
666
+ const outcome = budgetcheck({ budget: undefined, totals: { totaltokens: 1_000_000, cost: 10_000 } });
667
+ expect(outcome.allowed).toBe(true);
668
+ expect(outcome.halted).toBe(false);
669
+ });
670
+ });
671
+
672
+ /** Builds one plandraft fixture from a draft answer body. */
673
+ function outcomeof(body: string): plandraft {
674
+ const parsed = JSON.parse(body) as {
675
+ goal: string;
676
+ steps: Array<{ kind: string; target?: string; value?: string; summary: string }>;
677
+ openquestions: string[];
678
+ };
679
+ return {
680
+ id: "draft1",
681
+ goal: parsed.goal,
682
+ steps: parsed.steps.map((step, index) => ({
683
+ id: `step${index + 1}`,
684
+ kind: step.kind,
685
+ ...(step.target !== undefined ? { target: step.target } : {}),
686
+ ...(step.value !== undefined ? { value: step.value } : {}),
687
+ summary: step.summary,
688
+ })),
689
+ openquestions: parsed.openquestions,
690
+ providerid: "prov1",
691
+ model: "model-a",
692
+ state: "approved",
693
+ lintfindings: [],
694
+ createdat: now,
695
+ };
696
+ }