@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,706 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ agentbudgetof,
4
+ agentname,
5
+ agentscopeof,
6
+ budgetremaining,
7
+ broadcastrecipient,
8
+ consensusrecordof,
9
+ engagekillswitch,
10
+ escalationblock,
11
+ fleetoverview,
12
+ mailboxof,
13
+ outputcompare,
14
+ pauseagent,
15
+ readonlyscope,
16
+ recordverdict,
17
+ receivemessages,
18
+ resolverecipients,
19
+ reviewrecordof,
20
+ roleaddress,
21
+ sendmessage,
22
+ spendbudget,
23
+ unpauseagent,
24
+ unreadcount,
25
+ } from "../agent.js";
26
+ import type { agentidentity, agentmailbox, agentrecord, escalationrecord, runrecord, taskqueue } from "../types.js";
27
+
28
+ const now = 1_800_000_000_000;
29
+
30
+ /** Builds one fleet record fixture. */
31
+ function record(over: Partial<agentrecord> = {}): agentrecord {
32
+ return {
33
+ id: "a1",
34
+ name: "scout",
35
+ role: "worker",
36
+ origin: "https://example.com",
37
+ state: "active",
38
+ registeredat: now,
39
+ lastseenat: now,
40
+ ...over,
41
+ };
42
+ }
43
+
44
+ /** Builds one identity fixture for the routing layer. */
45
+ function identity(id: string, over: Partial<agentidentity> = {}): agentidentity {
46
+ return { id, name: `agent${id}`, role: "worker", depth: 0, state: "active", registeredat: now, ...over };
47
+ }
48
+
49
+ describe("torture: fleet naming discipline", () => {
50
+ it("refuses reserved words, duplicates, invalid identifiers and blanks", () => {
51
+ for (const reserved of ["user", "operator", "human", "system"]) {
52
+ expect(() =>
53
+ agentname({ records: [], id: "a1", proposed: reserved, origin: "https://example.com", now }),
54
+ ).toThrow(/reserved/i);
55
+ }
56
+ expect(() =>
57
+ agentname({
58
+ records: [record({ name: "scout" })],
59
+ id: "a2",
60
+ proposed: "Scout",
61
+ origin: "https://example.com",
62
+ now,
63
+ }),
64
+ ).toThrow(/already registered/i);
65
+ for (const invalid of ["", " ", "1abc", "-abc", "a b", "a-b", "a.b", "über", "abc!", "日本", "a_b"]) {
66
+ expect(() =>
67
+ agentname({ records: [], id: "a1", proposed: invalid, origin: "https://example.com", now }),
68
+ ).toThrow();
69
+ }
70
+ expect(agentname({ records: [], id: "a1", proposed: "A", origin: "https://example.com", now }).name).toBe("a");
71
+ expect(() => agentname({ records: [], id: "", proposed: "scout", origin: "https://example.com", now })).toThrow(
72
+ /needs its id/i,
73
+ );
74
+ expect(() => agentname({ records: [], id: "a1", proposed: "scout", origin: " ", now })).toThrow(/home origin/i);
75
+ });
76
+
77
+ it("lowercases and trims the proposal before validating", () => {
78
+ const named = agentname({ records: [], id: "a1", proposed: " Scout ", origin: "https://example.com", now });
79
+ expect(named.name).toBe("scout");
80
+ expect(named.role).toBe("worker");
81
+ expect(
82
+ agentname({ records: [], id: "a1", proposed: "scout", role: "reviewer", origin: "https://example.com", now })
83
+ .role,
84
+ ).toBe("reviewer");
85
+ });
86
+
87
+ it("accepts long single token identifiers and digits after the first letter", () => {
88
+ const longname = "a" + "1".repeat(64);
89
+ expect(agentname({ records: [], id: "a1", proposed: longname, origin: "https://example.com", now }).name).toBe(
90
+ longname,
91
+ );
92
+ expect(agentname({ records: [], id: "a1", proposed: "agent42", origin: "https://example.com", now }).name).toBe(
93
+ "agent42",
94
+ );
95
+ });
96
+ });
97
+
98
+ describe("torture: scope intersection and narrowing", () => {
99
+ it("refuses a request that lands entirely outside the grants", () => {
100
+ expect(() =>
101
+ agentscopeof({ agentid: "a1", requested: { origins: ["https://evil.com"] }, grants: ["https://example.com"] }),
102
+ ).toThrow(/never widens/i);
103
+ expect(() =>
104
+ agentscopeof({ agentid: "a1", requested: { actionkinds: ["nuke"] }, allowedkinds: ["readtext", "click"] }),
105
+ ).toThrow(/allowed catalog/i);
106
+ expect(() => agentscopeof({ agentid: " " })).toThrow(/agent id/i);
107
+ });
108
+
109
+ it("narrows partial requests while keeping the granted intersection", () => {
110
+ const scope = agentscopeof({
111
+ agentid: "a1",
112
+ requested: { origins: ["https://example.com", "https://evil.com"], actionkinds: ["readtext", "nuke"] },
113
+ grants: ["https://example.com", "https://shop.example"],
114
+ allowedkinds: ["readtext", "click"],
115
+ });
116
+ expect(scope.origins).toEqual(["https://example.com"]);
117
+ expect(scope.actionkinds).toEqual(["readtext"]);
118
+ });
119
+
120
+ it("keeps whitespace and empty entries out of every scope list", () => {
121
+ const scope = agentscopeof({
122
+ agentid: "a1",
123
+ requested: { origins: ["", " ", "https://example.com"], actionkinds: ["", " ", "readtext"] },
124
+ });
125
+ expect(scope.origins).toEqual(["https://example.com"]);
126
+ expect(scope.actionkinds).toEqual(["readtext"]);
127
+ });
128
+
129
+ it("an absent request stays unbounded while an empty request stays empty", () => {
130
+ const unbounded = agentscopeof({ agentid: "a1", grants: ["https://example.com"] });
131
+ expect(unbounded.origins).toEqual([]);
132
+ const empty = agentscopeof({ agentid: "a1", requested: { origins: [] }, grants: ["https://example.com"] });
133
+ expect(empty.origins).toEqual([]);
134
+ });
135
+
136
+ it("readonly scopes need their kinds and never carry origins", () => {
137
+ expect(() => readonlyscope({ agentid: "a1", readkinds: [] })).toThrow(/reads nothing/i);
138
+ expect(() => readonlyscope({ agentid: "a1", readkinds: ["", " "] })).toThrow(/reads nothing/i);
139
+ const scope = readonlyscope({ agentid: "a1", readkinds: ["readtext", " snapshot "] });
140
+ expect(scope.readonly).toBe(true);
141
+ expect(scope.origins).toEqual([]);
142
+ expect(scope.actionkinds).toEqual(["readtext", "snapshot"]);
143
+ expect(() => readonlyscope({ agentid: "", readkinds: ["readtext"] })).toThrow(/agent id/i);
144
+ });
145
+
146
+ it("lookalike origins never intersect a grant by suffix or subdomain tricks", () => {
147
+ expect(() =>
148
+ agentscopeof({
149
+ agentid: "a1",
150
+ requested: { origins: ["https://example.com.evil.com"] },
151
+ grants: ["https://example.com"],
152
+ }),
153
+ ).toThrow(/never widens/i);
154
+ expect(() =>
155
+ agentscopeof({
156
+ agentid: "a1",
157
+ requested: { origins: ["https://evil.com/example.com"] },
158
+ grants: ["https://example.com"],
159
+ }),
160
+ ).toThrow(/never widens/i);
161
+ const narrowed = agentscopeof({
162
+ agentid: "a1",
163
+ requested: { origins: ["https://example.com", "http://example.com", "https://example.com.evil.com"] },
164
+ grants: ["https://example.com"],
165
+ });
166
+ expect(narrowed.origins).toEqual(["https://example.com"]);
167
+ });
168
+ });
169
+
170
+ describe("torture: per agent budgets", () => {
171
+ it("refuses non positive and non finite ceilings", () => {
172
+ for (const bad of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
173
+ expect(() => agentbudgetof({ agentid: "a1", maxsteps: bad, now })).toThrow(/positive number/i);
174
+ expect(() => agentbudgetof({ agentid: "a1", maxtokens: bad, now })).toThrow(/positive number/i);
175
+ expect(() => agentbudgetof({ agentid: "a1", maxdurationms: bad, now })).toThrow(/positive number/i);
176
+ }
177
+ expect(() => agentbudgetof({ agentid: "", now })).toThrow(/agent id/i);
178
+ });
179
+
180
+ it("spends exactly to the ceiling and refuses one past it", () => {
181
+ const { state } = agentbudgetof({ agentid: "a1", maxsteps: 3, now });
182
+ let spent = spendbudget({ state, now: now + 1 });
183
+ spent = spendbudget({ state: spent, now: now + 2 });
184
+ spent = spendbudget({ state: spent, now: now + 3 });
185
+ expect(spent.spentsteps).toBe(3);
186
+ expect(() => spendbudget({ state: spent, now: now + 4 })).toThrow(/budgeted steps/i);
187
+ expect(budgetremaining(spent).steps).toBe(0);
188
+ });
189
+
190
+ it("refuses negative, non finite and absurd spends", () => {
191
+ const { state } = agentbudgetof({ agentid: "a1", now });
192
+ expect(() => spendbudget({ state, steps: -1, now })).toThrow(/finite non negative/i);
193
+ expect(() => spendbudget({ state, steps: Number.NaN, now })).toThrow(/finite non negative/i);
194
+ expect(() => spendbudget({ state, tokens: Number.POSITIVE_INFINITY, now })).toThrow(/finite non negative/i);
195
+ expect(() => spendbudget({ state, durationms: -0.5, now })).toThrow(/finite non negative/i);
196
+ expect(spendbudget({ state, steps: 0, tokens: 0, durationms: 0, now }).spentsteps).toBe(0);
197
+ });
198
+
199
+ it("token and duration ceilings refuse at their own boundary", () => {
200
+ const tokens = agentbudgetof({ agentid: "a1", maxtokens: 100, now }).state;
201
+ const spenttokens = spendbudget({ state: tokens, tokens: 100, now });
202
+ expect(() => spendbudget({ state: spenttokens, tokens: 1, now })).toThrow(/budgeted tokens/i);
203
+ const duration = agentbudgetof({ agentid: "a1", maxdurationms: 1_000, now }).state;
204
+ const spentduration = spendbudget({ state: duration, durationms: 1_000, now });
205
+ expect(() => spendbudget({ state: spentduration, durationms: 1, now })).toThrow(/budgeted milliseconds/i);
206
+ });
207
+
208
+ it("reports unbounded ceilings absent from the budget", () => {
209
+ const { state } = agentbudgetof({ agentid: "a1", now });
210
+ const remaining = budgetremaining(state);
211
+ expect(remaining.steps).toBeUndefined();
212
+ expect(remaining.tokens).toBeUndefined();
213
+ expect(remaining.durationms).toBeUndefined();
214
+ expect(remaining.reason).toMatch(/unbounded/i);
215
+ });
216
+ });
217
+
218
+ describe("torture: pause, unpause and the killswitch", () => {
219
+ it("pauses and unpauses only the named agent while unknown ids refuse", () => {
220
+ const fleet = [record({ id: "a1", name: "scout" }), record({ id: "a2", name: "worker" })];
221
+ const paused = pauseagent({ records: fleet, agentid: "a1", now: now + 1 });
222
+ expect(paused.find((entry) => entry.id === "a1")?.state).toBe("paused");
223
+ expect(paused.find((entry) => entry.id === "a2")?.state).toBe("active");
224
+ const resumed = unpauseagent({ records: paused, agentid: "a1", now: now + 2 });
225
+ expect(resumed.find((entry) => entry.id === "a1")?.state).toBe("active");
226
+ expect(() => unpauseagent({ records: fleet, agentid: "a1", now })).toThrow(/not paused/i);
227
+ expect(() => pauseagent({ records: fleet, agentid: "a3", now })).toThrow(/not registered/i);
228
+ expect(() => unpauseagent({ records: fleet, agentid: "a3", now })).toThrow(/not registered/i);
229
+ const stopped = [record({ id: "a1", name: "scout", state: "stopped" })];
230
+ expect(() => pauseagent({ records: stopped, agentid: "a1", now })).toThrow(/stopped/i);
231
+ });
232
+
233
+ it("double pause stays paused and double unpause of an active agent refuses", () => {
234
+ const fleet = [record()];
235
+ const once = pauseagent({ records: fleet, agentid: "a1", now: now + 1 });
236
+ const twice = pauseagent({ records: once, agentid: "a1", now: now + 2 });
237
+ expect(twice.find((entry) => entry.id === "a1")?.state).toBe("paused");
238
+ expect(() => unpauseagent({ records: [record()], agentid: "a1", now })).toThrow(/not paused/i);
239
+ });
240
+
241
+ it("the killswitch stops every agent, cancels every agent run and clears every queue", () => {
242
+ const fleet = [record({ id: "a1" }), record({ id: "a2", state: "paused" })];
243
+ const runs: runrecord[] = [
244
+ { runid: "r1", planid: "p1", sessionid: "s1", state: "running", createdat: now, updatedat: now, agentid: "a1" },
245
+ { runid: "r2", planid: "p1", sessionid: "s1", state: "running", createdat: now, updatedat: now, agentid: "a2" },
246
+ { runid: "r3", planid: "p1", sessionid: "s1", state: "completed", createdat: now, updatedat: now, agentid: "a1" },
247
+ { runid: "r4", planid: "p1", sessionid: "s1", state: "running", createdat: now, updatedat: now },
248
+ ];
249
+ const queues: taskqueue[] = [
250
+ {
251
+ lanes: [],
252
+ priorities: [],
253
+ completionpolicy: "all",
254
+ items: [{ id: "t1", lane: "default", priority: 1, payload: "work", state: "queued", enqueuedat: now }],
255
+ claims: [{ agentid: "a1", taskid: "t1", claimedat: now, heartbeatat: now }],
256
+ },
257
+ ];
258
+ const result = engagekillswitch({ records: fleet, runs, queues, now: now + 5 });
259
+ expect(result.records.every((entry) => entry.state === "stopped")).toBe(true);
260
+ expect(result.runs.find((run) => run.runid === "r1")?.state).toBe("cancelled");
261
+ expect(result.runs.find((run) => run.runid === "r2")?.state).toBe("cancelled");
262
+ expect(result.runs.find((run) => run.runid === "r3")?.state).toBe("completed");
263
+ expect(result.runs.find((run) => run.runid === "r4")?.state).toBe("running");
264
+ expect(result.queues[0]?.items).toEqual([]);
265
+ expect(result.queues[0]?.claims).toEqual([]);
266
+ expect(result.stopped.length).toBe(2);
267
+ expect(result.reason).toMatch(/killswitch/i);
268
+ });
269
+
270
+ it("fleet overview counts every control state exactly", () => {
271
+ const fleet = [
272
+ record({ id: "a1" }),
273
+ record({ id: "a2", state: "paused" }),
274
+ record({ id: "a3", state: "stopped" }),
275
+ record({ id: "a4", state: "stopped" }),
276
+ ];
277
+ const overview = fleetoverview({
278
+ records: fleet,
279
+ budgets: [],
280
+ openescalations: 2,
281
+ openreviews: 1,
282
+ killswitchat: now,
283
+ });
284
+ expect(overview).toMatchObject({
285
+ agents: 4,
286
+ active: 1,
287
+ paused: 1,
288
+ stopped: 2,
289
+ openescalations: 2,
290
+ openreviews: 1,
291
+ lastkillswitchat: now,
292
+ });
293
+ });
294
+ });
295
+
296
+ describe("torture: mailbox routing and delivery", () => {
297
+ it("direct messages refuse unknown recipients and self addressing", () => {
298
+ const agents = [identity("a1"), identity("a2")];
299
+ expect(() =>
300
+ sendmessage({
301
+ mailboxes: [],
302
+ agents,
303
+ id: "m1",
304
+ senderid: "a1",
305
+ recipient: "a3",
306
+ routing: "direct",
307
+ payload: "hello",
308
+ now,
309
+ }),
310
+ ).toThrow(/not registered/i);
311
+ expect(() =>
312
+ sendmessage({
313
+ mailboxes: [],
314
+ agents,
315
+ id: "m1",
316
+ senderid: "a1",
317
+ recipient: "a1",
318
+ routing: "direct",
319
+ payload: "hello",
320
+ now,
321
+ }),
322
+ ).toThrow(/own sender/i);
323
+ expect(() =>
324
+ sendmessage({
325
+ mailboxes: [],
326
+ agents,
327
+ id: "m1",
328
+ senderid: "a1",
329
+ recipient: "a2",
330
+ routing: "direct",
331
+ payload: " ",
332
+ now,
333
+ }),
334
+ ).toThrow(/needs its payload/i);
335
+ expect(() => resolverecipients({ agents, senderid: "a1", recipient: "a3", routing: "direct" })).toThrow(
336
+ /not registered/i,
337
+ );
338
+ });
339
+
340
+ it("broadcast reaches every live agent but the sender while stopped agents never receive", () => {
341
+ const agents = [identity("a1"), identity("a2"), identity("a3", { state: "stopped" })];
342
+ const recipients = resolverecipients({
343
+ agents,
344
+ senderid: "a1",
345
+ recipient: broadcastrecipient,
346
+ routing: "broadcast",
347
+ });
348
+ expect(recipients).toEqual(["a2"]);
349
+ const delivered = sendmessage({
350
+ mailboxes: [],
351
+ agents,
352
+ id: "m1",
353
+ senderid: "a1",
354
+ recipient: broadcastrecipient,
355
+ routing: "broadcast",
356
+ payload: "fleet update",
357
+ now,
358
+ });
359
+ expect(unreadcount(delivered, "a2")).toBe(1);
360
+ expect(unreadcount(delivered, "a1")).toBe(0);
361
+ expect(unreadcount(delivered, "a3")).toBe(0);
362
+ expect(mailboxof(delivered, "a1").outbox.length).toBe(1);
363
+ expect(mailboxof(delivered, "a3").inbox.length).toBe(0);
364
+ });
365
+
366
+ it("role routing reaches only the live agents of that role", () => {
367
+ const agents = [
368
+ identity("a1", { role: "worker" }),
369
+ identity("a2", { role: "reviewer" }),
370
+ identity("a3", { role: "worker", state: "stopped" }),
371
+ ];
372
+ expect(resolverecipients({ agents, senderid: "a1", recipient: "worker", routing: "role" })).toEqual(["a1"]);
373
+ expect(resolverecipients({ agents, senderid: "a1", recipient: "reviewer", routing: "role" })).toEqual(["a2"]);
374
+ expect(resolverecipients({ agents, senderid: "a1", recipient: "nonexistent", routing: "role" })).toEqual([]);
375
+ expect(roleaddress(agents, "worker")).toEqual(["a1"]);
376
+ });
377
+
378
+ it("receives drain resets the unread counter, marks read times and keeps read state on re-drain", () => {
379
+ const agents = [identity("a1"), identity("a2")];
380
+ let mailboxes: agentmailbox[] = sendmessage({
381
+ mailboxes: [],
382
+ agents,
383
+ id: "m1",
384
+ senderid: "a1",
385
+ recipient: "a2",
386
+ routing: "direct",
387
+ payload: "one",
388
+ now,
389
+ });
390
+ mailboxes = sendmessage({
391
+ mailboxes,
392
+ agents,
393
+ id: "m2",
394
+ senderid: "a1",
395
+ recipient: "a2",
396
+ routing: "direct",
397
+ payload: "two",
398
+ now: now + 1,
399
+ });
400
+ expect(unreadcount(mailboxes, "a2")).toBe(2);
401
+ const drained = receivemessages({ mailboxes, agentid: "a2", now: now + 10 });
402
+ expect(drained.messages.map((message) => message.payload)).toEqual(["one", "two"]);
403
+ expect(unreadcount(drained.mailboxes, "a2")).toBe(0);
404
+ expect(drained.messages.every((message) => message.readat === now + 10)).toBe(true);
405
+ const again = receivemessages({ mailboxes: drained.mailboxes, agentid: "a2", now: now + 20 });
406
+ expect(again.messages.every((message) => message.readat === now + 10)).toBe(true);
407
+ });
408
+
409
+ it("unicode, emoji and control payloads travel without loss", () => {
410
+ const agents = [identity("a1"), identity("a2")];
411
+ const payload = "こんにちは 🌍 \x00\x01<script>alert(1)</script> שלום";
412
+ const mailboxes = sendmessage({
413
+ mailboxes: [],
414
+ agents,
415
+ id: "m1",
416
+ senderid: "a1",
417
+ recipient: "a2",
418
+ routing: "direct",
419
+ payload,
420
+ now,
421
+ });
422
+ expect(mailboxof(mailboxes, "a2").inbox[0]?.payload).toBe(payload);
423
+ });
424
+ });
425
+
426
+ describe("torture: escalation holds and review verdicts", () => {
427
+ it("an open escalation blocks its agent while decided ones never block", () => {
428
+ const escalations: escalationrecord[] = [
429
+ {
430
+ id: "e1",
431
+ agentid: "a1",
432
+ subject: "payment flow needs a human",
433
+ state: "open",
434
+ raisedat: now,
435
+ context: "the checkout needs consent",
436
+ },
437
+ {
438
+ id: "e2",
439
+ agentid: "a2",
440
+ subject: "resolved",
441
+ state: "decided",
442
+ raisedat: now,
443
+ context: "",
444
+ decision: "approved",
445
+ },
446
+ ];
447
+ const blocked = escalationblock({ escalations, agentid: "a1" });
448
+ expect(blocked.blocked).toBe(true);
449
+ expect(blocked.open).toBe(1);
450
+ expect(blocked.reason).toMatch(/only the human answer lifts/i);
451
+ const free = escalationblock({ escalations, agentid: "a2" });
452
+ expect(free.blocked).toBe(false);
453
+ const unknown = escalationblock({ escalations, agentid: "a3" });
454
+ expect(unknown.blocked).toBe(false);
455
+ });
456
+
457
+ it("review requests refuse self review, blanks and unknown reviewers", () => {
458
+ expect(() =>
459
+ reviewrecordof({ id: "", fromagentid: "a1", toagentid: "a2", subject: "s", output: "o", now }),
460
+ ).toThrow(/needs its id/i);
461
+ expect(() =>
462
+ reviewrecordof({ id: "r1", fromagentid: "a1", toagentid: "a1", subject: "s", output: "o", now }),
463
+ ).toThrow(/never reviews its own/i);
464
+ expect(() =>
465
+ reviewrecordof({ id: "r1", fromagentid: "a1", toagentid: "a2", subject: " ", output: "o", now }),
466
+ ).toThrow(/subject/i);
467
+ expect(() =>
468
+ reviewrecordof({ id: "r1", fromagentid: "a1", toagentid: "a2", subject: "s", output: "", now }),
469
+ ).toThrow(/original output/i);
470
+ const open = reviewrecordof({
471
+ id: "r1",
472
+ fromagentid: "a1",
473
+ toagentid: "a2",
474
+ subject: "verify the totals",
475
+ output: "42",
476
+ now,
477
+ });
478
+ expect(open.state).toBe("open");
479
+ expect(() => recordverdict({ records: [open], id: "r2", reviewerid: "a2", verdict: "approve", now })).toThrow(
480
+ /does not exist/i,
481
+ );
482
+ expect(() => recordverdict({ records: [open], id: "r1", reviewerid: "a3", verdict: "approve", now })).toThrow(
483
+ /never answers a review addressed to another agent/i,
484
+ );
485
+ expect(() => recordverdict({ records: [open], id: "r1", reviewerid: "a2", verdict: "reject", now })).toThrow(
486
+ /names its issues/i,
487
+ );
488
+ });
489
+
490
+ it("verdicts answer once while rejections demand their issues", () => {
491
+ const open = reviewrecordof({
492
+ id: "r1",
493
+ fromagentid: "a1",
494
+ toagentid: "a2",
495
+ subject: "verify the totals",
496
+ output: "42",
497
+ now,
498
+ });
499
+ expect(() =>
500
+ recordverdict({ records: [open], id: "r1", reviewerid: "a2", verdict: "reject", now: now + 1 }),
501
+ ).toThrow(/names its issues/i);
502
+ expect(() =>
503
+ recordverdict({ records: [open], id: "r1", reviewerid: "a2", verdict: "changes", now: now + 1 }),
504
+ ).toThrow(/names its issues/i);
505
+ const answered = recordverdict({
506
+ records: [open],
507
+ id: "r1",
508
+ reviewerid: "a2",
509
+ verdict: "reject",
510
+ issues: ["the totals miss the tax row"],
511
+ now: now + 1,
512
+ });
513
+ expect(answered.state).toBe("answered");
514
+ expect(answered.verdict).toBe("reject");
515
+ expect(answered.issues).toEqual(["the totals miss the tax row"]);
516
+ expect(() =>
517
+ recordverdict({ records: [answered], id: "r1", reviewerid: "a2", verdict: "approve", now: now + 2 }),
518
+ ).toThrow(/never rewrites/i);
519
+ const approved = recordverdict({
520
+ records: [reviewrecordof({ id: "r2", fromagentid: "a1", toagentid: "a2", subject: "s", output: "o", now })],
521
+ id: "r2",
522
+ reviewerid: "a2",
523
+ verdict: "approve",
524
+ now: now + 1,
525
+ });
526
+ expect(approved.verdict).toBe("approve");
527
+ expect(approved.issues).toBeUndefined();
528
+ });
529
+
530
+ it("issue lists keep only meaningful entries", () => {
531
+ const open = reviewrecordof({ id: "r1", fromagentid: "a1", toagentid: "a2", subject: "s", output: "o", now });
532
+ const answered = recordverdict({
533
+ records: [open],
534
+ id: "r1",
535
+ reviewerid: "a2",
536
+ verdict: "reject",
537
+ issues: ["", " ", "real issue"],
538
+ now: now + 1,
539
+ });
540
+ expect(answered.issues).toEqual(["real issue"]);
541
+ });
542
+ });
543
+
544
+ describe("torture: output comparison", () => {
545
+ it("grades matching, conflicting and missing fields with sorted keys", () => {
546
+ const comparison = outputcompare({
547
+ id: "c1",
548
+ subject: "the pricing table",
549
+ left: { agentid: "a1", fields: { price: "42", tax: "8", note: "left only" } },
550
+ right: { agentid: "a2", fields: { price: "42", tax: "9", extra: "right only" } },
551
+ now,
552
+ });
553
+ expect(comparison.matching).toEqual(["price"]);
554
+ expect(comparison.conflicting).toEqual(["tax"]);
555
+ expect(comparison.missing.sort()).toEqual(["extra", "note"]);
556
+ });
557
+
558
+ it("refuses blanks, self comparison and unicode keys sort deterministically", () => {
559
+ expect(() =>
560
+ outputcompare({
561
+ id: "",
562
+ subject: "s",
563
+ left: { agentid: "a1", fields: {} },
564
+ right: { agentid: "a2", fields: {} },
565
+ now,
566
+ }),
567
+ ).toThrow(/needs its id/i);
568
+ expect(() =>
569
+ outputcompare({
570
+ id: "c1",
571
+ subject: " ",
572
+ left: { agentid: "a1", fields: {} },
573
+ right: { agentid: "a2", fields: {} },
574
+ now,
575
+ }),
576
+ ).toThrow(/subject/i);
577
+ expect(() =>
578
+ outputcompare({
579
+ id: "c1",
580
+ subject: "s",
581
+ left: { agentid: "a1", fields: {} },
582
+ right: { agentid: "a1", fields: {} },
583
+ now,
584
+ }),
585
+ ).toThrow(/never competes with itself/i);
586
+ const comparison = outputcompare({
587
+ id: "c1",
588
+ subject: "s",
589
+ left: { agentid: "a1", fields: { b: "1", a: "2", é: "3" } },
590
+ right: { agentid: "a2", fields: { b: "1", a: "9", é: "3" } },
591
+ now,
592
+ });
593
+ expect(comparison.matching.sort()).toEqual(["b", "é"].sort());
594
+ expect(comparison.conflicting).toEqual(["a"]);
595
+ });
596
+ });
597
+
598
+ describe("torture: consensus voting", () => {
599
+ it("refuses blank ids, proposals, quorums and votes", () => {
600
+ expect(() => consensusrecordof({ id: "", proposal: "p", votes: [], quorum: 1, now })).toThrow(/needs its id/i);
601
+ expect(() => consensusrecordof({ id: "c1", proposal: " ", votes: [], quorum: 1, now })).toThrow(/proposal/i);
602
+ expect(() => consensusrecordof({ id: "c1", proposal: "p", votes: [], quorum: 0, now })).toThrow(
603
+ /positive whole number/i,
604
+ );
605
+ expect(() => consensusrecordof({ id: "c1", proposal: "p", votes: [], quorum: 1.5, now })).toThrow(
606
+ /positive whole number/i,
607
+ );
608
+ expect(() => consensusrecordof({ id: "c1", proposal: "p", votes: [], quorum: -1, now })).toThrow(
609
+ /positive whole number/i,
610
+ );
611
+ expect(() =>
612
+ consensusrecordof({
613
+ id: "c1",
614
+ proposal: "p",
615
+ votes: [{ agentid: "", vote: "yes", castat: now }],
616
+ quorum: 1,
617
+ now,
618
+ }),
619
+ ).toThrow(/names its agent/i);
620
+ });
621
+
622
+ it("duplicate voters refuse loudly", () => {
623
+ const votes = [
624
+ { agentid: "a1", vote: "yes" as const, castat: now },
625
+ { agentid: "a1", vote: "yes" as const, castat: now + 1 },
626
+ ];
627
+ expect(() => consensusrecordof({ id: "c1", proposal: "adopt the plan", votes, quorum: 1, now })).toThrow(
628
+ /one per agent/i,
629
+ );
630
+ });
631
+
632
+ it("carries at exactly the quorum, fails only when complete and stays open otherwise", () => {
633
+ const twoyes = consensusrecordof({
634
+ id: "c1",
635
+ proposal: "p",
636
+ votes: [
637
+ { agentid: "a1", vote: "yes", castat: now },
638
+ { agentid: "a2", vote: "yes", castat: now },
639
+ ],
640
+ quorum: 2,
641
+ now,
642
+ });
643
+ expect(twoyes.outcome).toBe("carried");
644
+ expect(twoyes.closedat).toBe(now);
645
+ const oneyes = consensusrecordof({
646
+ id: "c1",
647
+ proposal: "p",
648
+ votes: [{ agentid: "a1", vote: "yes", castat: now }],
649
+ quorum: 2,
650
+ voters: 1,
651
+ now,
652
+ });
653
+ expect(oneyes.outcome).toBe("failed");
654
+ const incomplete = consensusrecordof({
655
+ id: "c1",
656
+ proposal: "p",
657
+ votes: [{ agentid: "a1", vote: "yes", castat: now }],
658
+ quorum: 2,
659
+ now,
660
+ });
661
+ expect(incomplete.outcome).toBe("open");
662
+ expect(incomplete.closedat).toBeUndefined();
663
+ const complete = consensusrecordof({
664
+ id: "c1",
665
+ proposal: "p",
666
+ votes: [
667
+ { agentid: "a1", vote: "yes", castat: now },
668
+ { agentid: "a2", vote: "no", castat: now },
669
+ ],
670
+ quorum: 2,
671
+ voters: 2,
672
+ now,
673
+ });
674
+ expect(complete.outcome).toBe("failed");
675
+ const abstaincarried = consensusrecordof({
676
+ id: "c1",
677
+ proposal: "p",
678
+ votes: [
679
+ { agentid: "a1", vote: "abstain", castat: now },
680
+ { agentid: "a2", vote: "yes", castat: now },
681
+ ],
682
+ quorum: 1,
683
+ voters: 2,
684
+ now,
685
+ });
686
+ expect(abstaincarried.outcome).toBe("carried");
687
+ });
688
+
689
+ it("tallies every vote kind and keeps the dissenting reasons", () => {
690
+ const round = consensusrecordof({
691
+ id: "c1",
692
+ proposal: "p",
693
+ votes: [
694
+ { agentid: "a1", vote: "yes", castat: now },
695
+ { agentid: "a2", vote: "no", reason: "the tax row is missing", castat: now + 1 },
696
+ { agentid: "a3", vote: "abstain", reason: "", castat: now + 2 },
697
+ ],
698
+ quorum: 5,
699
+ now,
700
+ });
701
+ expect(round.tally).toEqual({ yes: 1, no: 1, abstain: 1 });
702
+ expect(round.votes.find((vote) => vote.agentid === "a2")?.reason).toBe("the tax row is missing");
703
+ expect(round.votes.find((vote) => vote.agentid === "a3")?.reason).toBeUndefined();
704
+ expect(round.outcome).toBe("open");
705
+ });
706
+ });