@wenathlan/devthink 2.0.14 → 2.0.19

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 +60 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
@@ -1,1245 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- ackreview,
4
- applyreview,
5
- arbitrate,
6
- assignwork,
7
- boardstate,
8
- boardsummary,
9
- castvote,
10
- checkclaim,
11
- claim,
12
- claimheartbeat,
13
- collectresults,
14
- consensusstate,
15
- complete,
16
- canceltask,
17
- electleader,
18
- emptyboard,
19
- emptyqueue,
20
- enqueue,
21
- entryfresh,
22
- escalate,
23
- inheritconsent,
24
- lanereport,
25
- openconsensus,
26
- postentry,
27
- queuecomplete,
28
- readentries,
29
- requeue,
30
- requestreview,
31
- resolveescalation,
32
- retireentries,
33
- retireentry,
34
- scaleworkers,
35
- steal,
36
- sweepreviews,
37
- taskcounts,
38
- plannersplit,
39
- reportstep,
40
- blackboardsections,
41
- } from "../swarm.js";
42
- import type {
43
- agentidentity,
44
- arbitrationrule,
45
- blackboard,
46
- blackboardentry,
47
- consensusround,
48
- escalationrecord,
49
- reviewrequest,
50
- taskqueue,
51
- } from "../types.js";
52
-
53
- const now = 1_800_000_000_000;
54
-
55
- /** Builds one agent identity fixture of the multiagent family. */
56
- function agent(id: string, over: Partial<agentidentity> = {}): agentidentity {
57
- return {
58
- id,
59
- name: `agent ${id}`,
60
- role: "worker",
61
- depth: 0,
62
- state: "active",
63
- registeredat: now,
64
- heartbeatat: now,
65
- ...over,
66
- };
67
- }
68
-
69
- /** Builds one queue with two queued tasks. */
70
- function queue(tasks = ["t1", "t2"]): taskqueue {
71
- let state = emptyqueue({ lanes: ["main", "side"], priorities: [1, 5], completionpolicy: "all" });
72
- for (const [index, id] of tasks.entries())
73
- state = enqueuehelper(state, id, index % 2 === 0 ? "main" : "side", index, `payload ${id}`);
74
- return state;
75
- }
76
-
77
- function enqueuehelper(queue: taskqueue, id: string, lane: string, priority: number, payload: string): taskqueue {
78
- return enqueue({ queue, id, lane, priority, payload, now: now + priority });
79
- }
80
-
81
- describe("torture: swarm leader election and work assignment", () => {
82
- it("elects the first registered live agent by default and refuses the empty swarm", () => {
83
- const topology = electleader({
84
- agents: [agent("a1"), agent("a2", { role: "critic" }), agent("a3", { role: "verifier" })],
85
- id: "top1",
86
- now,
87
- });
88
- expect(topology.leaderid).toBe("a3");
89
- expect(topology.workerids).toEqual(["a1"]);
90
- expect(topology.criticids).toEqual(["a2"]);
91
- expect(topology.verifierids).toEqual([]);
92
- expect(() => electleader({ agents: [], id: "top1", now })).toThrow(/no live agent/i);
93
- expect(() => electleader({ agents: [agent("a1", { state: "stopped" })], id: "top1", now })).toThrow(
94
- /no live agent/i,
95
- );
96
- });
97
-
98
- it("elects the named agent the user named and refuses the missing or blank name", () => {
99
- const topology = electleader({
100
- agents: [agent("a1"), agent("a2")],
101
- id: "top1",
102
- rule: { kind: "named", agentid: "a1" },
103
- now,
104
- });
105
- expect(topology.leaderid).toBe("a1");
106
- expect(() =>
107
- electleader({ agents: [agent("a1")], id: "top1", rule: { kind: "named", agentid: "ghost" }, now }),
108
- ).toThrow(/not a live agent/i);
109
- expect(() =>
110
- electleader({ agents: [agent("a1")], id: "top1", rule: { kind: "named", agentid: " " }, now }),
111
- ).toThrow(/needs the agent id/i);
112
- });
113
-
114
- it("slices the queued and claimed tasks across the workers in round robin order", () => {
115
- const topology = electleader({
116
- agents: [agent("a1"), agent("w1"), agent("w2")],
117
- id: "top1",
118
- rule: { kind: "named", agentid: "a1" },
119
- now,
120
- });
121
- const assigned = assignwork({
122
- topology,
123
- tasks: [
124
- { id: "t1", lane: "main", priority: 1, payload: "p1", state: "queued", enqueuedat: now },
125
- { id: "t2", lane: "side", priority: 1, payload: "p2", state: "queued", enqueuedat: now + 1 },
126
- { id: "t3", lane: "main", priority: 1, payload: "p3", state: "done", enqueuedat: now + 2 },
127
- ],
128
- now: now + 10,
129
- });
130
- expect(assigned.assignments).toHaveLength(2);
131
- expect(assigned.assignments[0]?.workerid).toBe("w1");
132
- expect(assigned.assignments[1]?.workerid).toBe("w2");
133
- expect(() => assignwork({ topology: { ...topology, workerids: [] }, tasks: [], now })).toThrow(/no worker/i);
134
- });
135
-
136
- it("collects the outputs and names the missing assignments", () => {
137
- const topology = electleader({
138
- agents: [agent("a1"), agent("w1")],
139
- id: "top1",
140
- rule: { kind: "named", agentid: "a1" },
141
- now,
142
- });
143
- const assigned = assignwork({
144
- topology,
145
- tasks: [{ id: "t1", lane: "main", priority: 1, payload: "p1", state: "queued", enqueuedat: now }],
146
- now,
147
- });
148
- const collected = collectresults({ topology: assigned, outputs: [] });
149
- expect(collected.missing).toEqual(["w1:t1"]);
150
- const answered = collectresults({
151
- topology: assigned,
152
- outputs: [{ workerid: "w1", taskid: "t1", state: "done", summary: "the result" }],
153
- });
154
- expect(answered.missing).toEqual([]);
155
- expect(answered.gathered[0]?.summary).toBe("the result");
156
- });
157
-
158
- it("scales the workers up to the pending load and down to the idle level", () => {
159
- const agents = [agent("a1"), agent("w1"), agent("w2"), agent("w3")];
160
- const topology = electleader({ agents, id: "top1", rule: { kind: "named", agentid: "a1" }, now });
161
- const downscaled = scaleworkers({ topology, agents, pending: 1, now: now + 1 });
162
- expect(downscaled.retired).toHaveLength(2);
163
- expect(downscaled.topology.workerids).toHaveLength(1);
164
- const scaled = scaleworkers({ topology: downscaled.topology, agents, pending: 3, now: now + 2 });
165
- expect(scaled.added).toContain("w3");
166
- expect(scaled.topology.workerids).toHaveLength(3);
167
- const bounded = scaleworkers({ topology: downscaled.topology, agents, pending: 5, bound: 2, now: now + 3 });
168
- expect(bounded.topology.workerids).toHaveLength(2);
169
- expect(bounded.added).toHaveLength(1);
170
- });
171
- });
172
-
173
- describe("torture: swarm planner executor split and reviews", () => {
174
- it("splits one task between the planner and the executor and refuses the same agent on both sides", () => {
175
- const split = plannersplit({ id: "split1", planownerid: "p1", runownerid: "e1", taskid: "t1", now });
176
- expect(split.planownerid).toBe("p1");
177
- expect(split.runownerid).toBe("e1");
178
- expect(split.taskid).toBe("t1");
179
- expect(() => plannersplit({ id: "split1", planownerid: "same", runownerid: "same", now })).toThrow(
180
- /different agents/i,
181
- );
182
- expect(() => plannersplit({ id: "split1", planownerid: "", runownerid: "e", now })).toThrow(
183
- /plan owner and run owner/i,
184
- );
185
- });
186
-
187
- it("reports the executor step outcome and deduplicates the repeated stepid", () => {
188
- const split = plannersplit({ id: "split1", planownerid: "p1", runownerid: "e1", now });
189
- const reported = reportstep({ split, stepid: "s1", outcome: "done", detail: "fine", now: now + 1 });
190
- expect(reported.stepreports).toHaveLength(1);
191
- const repeated = reportstep({
192
- split: reported,
193
- stepid: "s1",
194
- outcome: "failed",
195
- detail: "the retry failed",
196
- now: now + 2,
197
- });
198
- expect(repeated.stepreports).toHaveLength(1);
199
- expect(repeated.stepreports[0]?.outcome).toBe("failed");
200
- expect(() => reportstep({ split, stepid: " ", outcome: "done", detail: "x", now })).toThrow(/step id/i);
201
- expect(() => reportstep({ split, stepid: "s1", outcome: "done", detail: " ", now })).toThrow(/detail/i);
202
- });
203
-
204
- it("requests, acks and answers the review while the foreign reviewer never answers", () => {
205
- const requests = requestreview({
206
- requests: [],
207
- id: "rev1",
208
- fromagentid: "a1",
209
- toagentid: "a2",
210
- subject: "the subject",
211
- payload: "the payload",
212
- timeoutms: 1000,
213
- now,
214
- });
215
- expect(requests[0]?.state).toBe("open");
216
- expect(() =>
217
- requestreview({ requests: [], id: "rev1", fromagentid: "a1", toagentid: "a1", subject: "s", payload: "p", now }),
218
- ).toThrow(/another reviewing agent/i);
219
- expect(() =>
220
- requestreview({ requests: [], id: "rev1", fromagentid: "a1", toagentid: "a2", subject: " ", payload: "p", now }),
221
- ).toThrow(/subject/i);
222
- const acked = ackreview({ requests, id: "rev1", now: now + 1 });
223
- expect(acked[0]?.state).toBe("acked");
224
- expect(() => ackreview({ requests: acked, id: "rev1", now: now + 2 })).toThrow(/only an open request/i);
225
- expect(() => ackreview({ requests, id: "ghost", now })).toThrow(/does not exist/i);
226
- const answered = applyreview({
227
- requests: acked,
228
- id: "rev1",
229
- reviewerid: "a2",
230
- verdict: "approve",
231
- issues: [],
232
- requiredchanges: [],
233
- now: now + 3,
234
- });
235
- expect(answered.review.verdict).toBe("approve");
236
- expect(answered.requests[0]?.state).toBe("answered");
237
- expect(() =>
238
- applyreview({
239
- requests: acked,
240
- id: "rev1",
241
- reviewerid: "a3",
242
- verdict: "approve",
243
- issues: [],
244
- requiredchanges: [],
245
- now,
246
- }),
247
- ).toThrow(/never answers in its place/i);
248
- expect(() =>
249
- applyreview({
250
- requests: acked,
251
- id: "rev1",
252
- reviewerid: "a2",
253
- verdict: "changes",
254
- issues: [],
255
- requiredchanges: [],
256
- now,
257
- }),
258
- ).toThrow(/required changes/i);
259
- });
260
-
261
- it("sweeps the timed out review requests and keeps the answered ones", () => {
262
- const requests: reviewrequest[] = [
263
- {
264
- id: "rev1",
265
- fromagentid: "a1",
266
- toagentid: "a2",
267
- subject: "s",
268
- payload: "p",
269
- state: "open",
270
- requestedat: now,
271
- timeoutat: now + 100,
272
- },
273
- {
274
- id: "rev2",
275
- fromagentid: "a1",
276
- toagentid: "a3",
277
- subject: "s",
278
- payload: "p",
279
- state: "answered",
280
- requestedat: now,
281
- answeredat: now + 1,
282
- },
283
- ];
284
- const swept = sweepreviews({ requests, now: now + 200 });
285
- expect(swept.timedout).toEqual(["rev1"]);
286
- expect(swept.requests[0]?.state).toBe("timeout");
287
- expect(swept.requests[1]?.state).toBe("answered");
288
- expect(sweepreviews({ requests, now: now + 50 }).timedout).toEqual([]);
289
- expect(
290
- sweepreviews({
291
- requests: [
292
- { id: "rev3", fromagentid: "a", toagentid: "b", subject: "s", payload: "p", state: "open", requestedat: now },
293
- ],
294
- now: now + 1000,
295
- }).timedout,
296
- ).toEqual([]);
297
- });
298
-
299
- it("records the verifier check of a result claim with the pass and the fail outcomes", () => {
300
- const check = checkclaim({
301
- id: "v1",
302
- verifierid: "v",
303
- claimagentid: "a1",
304
- claim: "the table holds 14 rows",
305
- method: "read the table again",
306
- outcome: "pass",
307
- evidence: "the second read found 14 rows",
308
- taskid: "t1",
309
- now,
310
- });
311
- expect(check.outcome).toBe("pass");
312
- expect(check.evidence).toBe("the second read found 14 rows");
313
- expect(() =>
314
- checkclaim({ id: "v1", verifierid: "v", claimagentid: "a1", claim: " ", method: "m", outcome: "pass", now }),
315
- ).toThrow(/claim/i);
316
- expect(() =>
317
- checkclaim({ id: "v1", verifierid: "v", claimagentid: "a1", claim: "c", method: " ", outcome: "pass", now }),
318
- ).toThrow(/method/i);
319
- expect(() =>
320
- checkclaim({ id: "v1", verifierid: "v", claimagentid: " ", claim: "c", method: "m", outcome: "pass", now }),
321
- ).toThrow(/names the agent/i);
322
- });
323
- });
324
-
325
- describe("torture: swarm board, escalation, arbitration and consensus", () => {
326
- it("builds the progressboard with one lane per live agent and the current task", () => {
327
- const q = queue();
328
- const claimed = claim({ queue: q, agentid: "a1", now }).queue;
329
- const board = boardstate({
330
- agents: [agent("a1"), agent("a2", { state: "stopped" })],
331
- queue: claimed,
332
- now: now + 1,
333
- });
334
- expect(board.lanes).toHaveLength(1);
335
- expect(board.lanes[0]?.agentid).toBe("a1");
336
- expect(board.lanes[0]?.currenttask).toBe("payload t2");
337
- });
338
-
339
- it("lifts one stalled decision to the user and resolves it with the user words", () => {
340
- const escalation = escalate({
341
- id: "esc1",
342
- agentid: "a1",
343
- subject: "the form refuses",
344
- context: "the form rejects the submit after the second attempt",
345
- now,
346
- });
347
- expect(escalation.state).toBe("open");
348
- expect(() => escalate({ id: "esc1", agentid: "a1", subject: " ", context: "c", now })).toThrow(/subject/i);
349
- expect(() => escalate({ id: "esc1", agentid: "a1", subject: "s", context: " ", now })).toThrow(/context/i);
350
- const resolved = resolveescalation({
351
- escalation,
352
- decision: "skip the submit and report the failure",
353
- now: now + 1,
354
- });
355
- expect(resolved.state).toBe("decided");
356
- expect(() => resolveescalation({ escalation: resolved, decision: "again", now })).toThrow(/already carries/i);
357
- expect(() => resolveescalation({ escalation, decision: " ", now })).toThrow(/words the user wrote/i);
358
- });
359
-
360
- it("arbitrates the competing claims by the priority, the age and the leader strategies", () => {
361
- const claims = [
362
- { agentid: "a3", claimedat: now },
363
- { agentid: "a1", claimedat: now + 1 },
364
- { agentid: "a2", claimedat: now + 2 },
365
- ];
366
- const priorityrule: arbitrationrule = {
367
- id: "r1",
368
- strategy: "priority",
369
- priorityorder: ["a1", "a2", "a3"],
370
- configuredat: now,
371
- };
372
- expect(arbitrate({ rule: priorityrule, claims })).toEqual(["a1", "a2", "a3"]);
373
- expect(arbitrate({ rule: { id: "r2", strategy: "age", priorityorder: [], configuredat: now }, claims })).toEqual([
374
- "a3",
375
- "a1",
376
- "a2",
377
- ]);
378
- expect(
379
- arbitrate({
380
- rule: { id: "r3", strategy: "leader", priorityorder: [], configuredat: now },
381
- leaderid: "a2",
382
- claims,
383
- }),
384
- ).toEqual(["a2", "a3", "a1"]);
385
- expect(() =>
386
- arbitrate({ rule: { id: "r3", strategy: "leader", priorityorder: [], configuredat: now }, claims }),
387
- ).toThrow(/needs the elected leader/i);
388
- expect(arbitrate({ rule: priorityrule, claims: [] })).toEqual([]);
389
- });
390
-
391
- it("opens one consensus round, collects the votes and reads the state", () => {
392
- const round = openconsensus({ id: "con1", subject: "the table is ready", quorum: 2, now });
393
- expect(round.state).toBe("open");
394
- expect(() => openconsensus({ id: "con1", subject: " ", quorum: 1, now })).toThrow(/subject/i);
395
- expect(() => openconsensus({ id: "con1", subject: "s", quorum: 0, now })).toThrow(/positive whole number/i);
396
- expect(() => openconsensus({ id: "con1", subject: "s", quorum: 1.5, now })).toThrow(/positive whole number/i);
397
- const voted = castvote({ round, agentid: "a1", vote: "yes", now: now + 1 });
398
- expect(voted.state).toBe("open");
399
- const carried = castvote({ round: voted, agentid: "a2", vote: "yes", now: now + 2 });
400
- expect(carried.state).toBe("carried");
401
- expect(() => castvote({ round: carried, agentid: "a3", vote: "yes", now: now + 3 })).toThrow(/closed round/i);
402
- expect(() => castvote({ round: voted, agentid: "a1", vote: "yes", now: now + 3 })).toThrow(/already voted/i);
403
- const state = consensusstate(carried);
404
- expect(state).toMatchObject({ yes: 2, no: 0, abstain: 0, quorum: 2, state: "carried" });
405
- });
406
-
407
- it("fails the consensus round when the no votes reach the quorum", () => {
408
- const round = openconsensus({ id: "con1", subject: "the risky step", quorum: 1, now });
409
- const failed = castvote({ round, agentid: "a1", vote: "no", now: now + 1 });
410
- expect(failed.state).toBe("failed");
411
- expect(consensusstate(failed).no).toBe(1);
412
- });
413
- });
414
-
415
- describe("torture: swarm task queue claim, steal and completion", () => {
416
- it("enqueues the tasks into the configured lanes and refuses the duplicate ids and unknown lanes", () => {
417
- const q = queue();
418
- expect(q.items).toHaveLength(2);
419
- expect(() => enqueuehelper(q, "t1", "main", 1, "p")).toThrow(/already waits/i);
420
- expect(() => enqueuehelper(q, "t3", "ghost", 1, "p")).toThrow(/not one of the configured lanes/i);
421
- expect(() => enqueuehelper(q, "t4", "main", 1, " ")).toThrow(/payload/i);
422
- const open = enqueue({ queue: emptyqueue(), id: "any", lane: "anylane", priority: 1, payload: "p", now });
423
- expect(open.items).toHaveLength(1);
424
- });
425
-
426
- it("claims the highest priority queued task first and holds one task per agent", () => {
427
- const q = queue(["t1", "t2"]);
428
- const first = claim({ queue: q, agentid: "a1", now: now + 1 });
429
- expect(first.task?.id).toBe("t2");
430
- const again = claim({ queue: first.queue, agentid: "a1", now: now + 2 });
431
- expect(again.task?.id).toBe("t2");
432
- expect(claim({ queue: q, agentid: "a2", now: now + 3 }).task?.id).toBe("t2");
433
- expect(claim({ queue: emptyqueue(), agentid: "a1", now }).task).toBeUndefined();
434
- });
435
-
436
- it("steals from the named lane under the ownership rules and refuses the foreign role", () => {
437
- const q = queue(["t1", "t2"]);
438
- const stolen = steal({
439
- queue: q,
440
- agentid: "a1",
441
- role: "worker",
442
- fromlane: "main",
443
- ownership: [{ lane: "main", roles: ["worker", "planner"] }],
444
- now: now + 1,
445
- });
446
- expect(stolen.task?.lane).toBe("main");
447
- expect(() =>
448
- steal({
449
- queue: q,
450
- agentid: "a1",
451
- role: "critic",
452
- fromlane: "main",
453
- ownership: [{ lane: "main", roles: ["worker"] }],
454
- now,
455
- }),
456
- ).toThrow(/stays out/i);
457
- expect(steal({ queue: q, agentid: "a1", role: "worker", fromlane: "side", now: now + 2 }).task?.lane).toBe("side");
458
- expect(steal({ queue: emptyqueue(), agentid: "a1", role: "worker", fromlane: "main", now }).task).toBeUndefined();
459
- });
460
-
461
- it("completes the claimed task and refuses the queued or unknown task", () => {
462
- const q = queue(["t1"]);
463
- const claimed = claim({ queue: q, agentid: "a1", now });
464
- const done = complete({ queue: claimed.queue, taskid: "t1", now: now + 1 });
465
- expect(done.items[0]?.state).toBe("done");
466
- expect(done.claims).toHaveLength(0);
467
- expect(() => complete({ queue: done, taskid: "t1", now })).toThrow(/only a claimed task/i);
468
- expect(() => complete({ queue: q, taskid: "t1", now })).toThrow(/only a claimed task/i);
469
- expect(() => complete({ queue: q, taskid: "ghost", now })).toThrow(/not in the queue/i);
470
- });
471
-
472
- it("cancels the queued and claimed tasks and refuses the done task", () => {
473
- const q = queue(["t1"]);
474
- expect(canceltask({ queue: q, taskid: "t1", now }).items[0]?.state).toBe("cancelled");
475
- const claimed = claim({ queue: q, agentid: "a1", now });
476
- const cancelled = canceltask({ queue: claimed.queue, taskid: "t1", now });
477
- expect(cancelled.items[0]?.state).toBe("cancelled");
478
- expect(cancelled.claims).toHaveLength(0);
479
- const done = complete({ queue: claimed.queue, taskid: "t1", now: now + 1 });
480
- expect(() => canceltask({ queue: done, taskid: "t1", now })).toThrow(/completed task never cancels/i);
481
- expect(() => canceltask({ queue: q, taskid: "ghost", now })).toThrow(/not in the queue/i);
482
- });
483
-
484
- it("refreshes the claim heartbeats of one agent and requeues the orphaned tasks past the window", () => {
485
- const q = queue(["t1"]);
486
- const claimed = claim({ queue: q, agentid: "a1", now });
487
- const beaten = claimheartbeat({ queue: claimed.queue, agentid: "a1", now: now + 50 });
488
- expect(beaten.claims[0]?.heartbeatat).toBe(now + 50);
489
- expect(requeue({ queue: beaten, now: now + 100, window: 200 }).requeued).toEqual([]);
490
- const orphaned = requeue({ queue: beaten, now: now + 300, window: 200 });
491
- expect(orphaned.requeued).toEqual(["t1"]);
492
- expect(orphaned.queue.items[0]?.state).toBe("queued");
493
- expect(orphaned.queue.claims).toHaveLength(0);
494
- expect(requeue({ queue: claimed.queue, now: now + 1000 }).requeued).toEqual([]);
495
- });
496
-
497
- it("reports the lane counts and the task totals with the claims", () => {
498
- const q = queue(["t1", "t2"]);
499
- const claimed = claim({ queue: q, agentid: "a1", now });
500
- const report = lanereport(claimed.queue);
501
- expect(report.find((lane) => lane.lane === "side")?.claimed).toBe(1);
502
- expect(report.find((lane) => lane.lane === "main")?.queued).toBe(1);
503
- expect(taskcounts(claimed.queue)).toMatchObject({ queued: 1, claimed: 1, done: 0, cancelled: 0 });
504
- });
505
-
506
- it("evaluates the completion policy of all and any over the queue", () => {
507
- const q = queue(["t1"]);
508
- expect(queuecomplete(q)).toBe(false);
509
- const claimed = claim({ queue: q, agentid: "a1", now });
510
- const done = complete({ queue: claimed.queue, taskid: "t1", now: now + 1 });
511
- expect(queuecomplete(done)).toBe(true);
512
- const anyqueue = emptyqueue({ completionpolicy: "any" });
513
- const withitems = enqueuehelper(enqueuehelper(anyqueue, "t1", "main", 1, "p1"), "t2", "main", 1, "p2");
514
- const firstdone = complete({
515
- queue: claim({ queue: withitems, agentid: "a1", now }).queue,
516
- taskid: "t1",
517
- now: now + 1,
518
- });
519
- expect(queuecomplete(firstdone)).toBe(true);
520
- expect(queuecomplete(emptyqueue())).toBe(false);
521
- });
522
- });
523
-
524
- describe("torture: swarm blackboard shared memory", () => {
525
- it("builds the empty board over the four shared sections", () => {
526
- const board = emptyboard();
527
- expect(board.sections).toEqual(blackboardsections);
528
- expect(board.entries).toEqual([]);
529
- expect(emptyboard(["goals"]).sections).toEqual(["goals"]);
530
- });
531
-
532
- it("posts an entry with its author and refuses the blank fields, the unknown section and the malformed json", () => {
533
- const posted = postentry({
534
- board: emptyboard(),
535
- id: "e1",
536
- key: "pricing.url",
537
- value: "https://example.com/pricing",
538
- section: "facts",
539
- author: "a1",
540
- now,
541
- });
542
- expect(posted.entries).toHaveLength(1);
543
- expect(posted.entries[0]).toMatchObject({
544
- key: "pricing.url",
545
- author: "a1",
546
- section: "facts",
547
- consentclass: "read",
548
- valuekind: "text",
549
- });
550
- expect(() =>
551
- postentry({ board: emptyboard(), id: "e1", key: " ", value: "v", section: "facts", author: "a1", now }),
552
- ).toThrow(/key/i);
553
- expect(() =>
554
- postentry({ board: emptyboard(), id: "e1", key: "k", value: " ", section: "facts", author: "a1", now }),
555
- ).toThrow(/value/i);
556
- expect(() =>
557
- postentry({ board: emptyboard(), id: "e1", key: "k", value: "v", section: "facts", author: " ", now }),
558
- ).toThrow(/author/i);
559
- expect(() =>
560
- postentry({ board: emptyboard(), id: "e1", key: "k", value: "v", section: "secret" as never, author: "a1", now }),
561
- ).toThrow(/shared blackboard sections/i);
562
- expect(() =>
563
- postentry({
564
- board: emptyboard(),
565
- id: "e1",
566
- key: "k",
567
- value: "{not json",
568
- section: "facts",
569
- author: "a1",
570
- valuekind: "json",
571
- now,
572
- }),
573
- ).toThrow(/json/i);
574
- expect(() =>
575
- postentry({ board: posted, id: "e1", key: "k2", value: "v", section: "facts", author: "a1", now }),
576
- ).toThrow(/already exists/i);
577
- });
578
-
579
- it("reads the entries with the section and freshness filters sorted by the newest first", () => {
580
- let board = postentry({
581
- board: emptyboard(),
582
- id: "e1",
583
- key: "goal",
584
- value: "extract the pricing",
585
- section: "goals",
586
- author: "a1",
587
- now: now,
588
- });
589
- board = postentry({
590
- board,
591
- id: "e2",
592
- key: "fact",
593
- value: "the table holds 14 rows",
594
- section: "facts",
595
- author: "a2",
596
- now: now + 100,
597
- });
598
- expect(readentries({ board, now: now + 200 })[0]?.id).toBe("e2");
599
- expect(readentries({ board, section: "goals", now })[0]?.id).toBe("e1");
600
- expect(readentries({ board, freshness: 150, now: now + 200 }).map((e) => e.id)).toEqual(["e2"]);
601
- expect(readentries({ board, freshness: 50, now: now + 200 }).map((e) => e.id)).toEqual([]);
602
- });
603
-
604
- it("retires the entries by the window and by their id while the retired entry stays stored", () => {
605
- let board = postentry({
606
- board: { ...emptyboard(), retirementwindow: 100 },
607
- id: "e1",
608
- key: "old",
609
- value: "v",
610
- section: "facts",
611
- author: "a1",
612
- now: now - 200,
613
- });
614
- board = postentry({ board, id: "e2", key: "new", value: "v", section: "facts", author: "a1", now });
615
- const retired = retireentries({ board, now });
616
- expect(retired.retired).toEqual(["e1"]);
617
- expect(retired.board.entries.find((e) => e.id === "e1")?.retiredat).toBe(now);
618
- expect(readentries({ board: retired.board, now })).toHaveLength(1);
619
- const single = retireentry({ board, entryid: "e2", now });
620
- expect(single.entries.find((e) => e.id === "e2")?.retiredat).toBe(now);
621
- expect(() => retireentry({ board: single, entryid: "e2", now })).toThrow(/already retired/i);
622
- expect(() => retireentry({ board, entryid: "ghost", now })).toThrow(/does not exist/i);
623
- });
624
-
625
- it("reads the entry freshness with the retirement and the window", () => {
626
- const entry: blackboardentry = {
627
- id: "e1",
628
- key: "k",
629
- valuekind: "text",
630
- value: "v",
631
- author: "a1",
632
- section: "facts",
633
- consentclass: "read",
634
- postedat: now,
635
- };
636
- expect(entryfresh(entry, now + 100, undefined)).toBe(true);
637
- expect(entryfresh(entry, now + 100, 50)).toBe(false);
638
- expect(entryfresh({ ...entry, retiredat: now }, now, undefined)).toBe(false);
639
- });
640
-
641
- it("inherits the consent class of the source extraction into the entry", () => {
642
- const entry: blackboardentry = {
643
- id: "e1",
644
- key: "k",
645
- valuekind: "text",
646
- value: "v",
647
- author: "a1",
648
- section: "findings",
649
- consentclass: "read",
650
- postedat: now,
651
- };
652
- expect(inheritconsent(entry, "sensitive").consentclass).toBe("sensitive");
653
- expect(inheritconsent(entry, "read").consentclass).toBe("read");
654
- });
655
-
656
- it("summarizes the board with the entry counts, the authors and the freshest posting time", () => {
657
- let board = postentry({
658
- board: emptyboard(),
659
- id: "e1",
660
- key: "k1",
661
- value: "v",
662
- section: "goals",
663
- author: "a1",
664
- now: now,
665
- });
666
- board = postentry({ board, id: "e2", key: "k2", value: "v", section: "goals", author: "a2", now: now + 1 });
667
- const summary = boardsummary(board, now + 2);
668
- const goals = summary.find((section) => section.section === "goals");
669
- expect(goals?.entries).toBe(2);
670
- expect(goals?.authors).toEqual(["a2", "a1"]);
671
- expect(goals?.freshestat).toBe(now + 1);
672
- expect(summary.find((section) => section.section === "facts")?.entries).toBe(0);
673
- });
674
- });
675
-
676
- describe("torture: swarm election rules, slices and missing results", () => {
677
- it("keeps the leader out of the worker, critic and verifier lanes whatever its role", () => {
678
- const topology = electleader({
679
- agents: [agent("w1"), agent("c1", { role: "critic" }), agent("v1", { role: "verifier" })],
680
- id: "top1",
681
- rule: { kind: "named", agentid: "c1" },
682
- now,
683
- });
684
- expect(topology.leaderid).toBe("c1");
685
- expect(topology.criticids).toEqual([]);
686
- expect(topology.workerids).toEqual(["w1"]);
687
- expect(topology.verifierids).toEqual(["v1"]);
688
- expect(topology.rule).toEqual({ kind: "named", agentid: "c1" });
689
- });
690
-
691
- it("refuses the named election of a stopped agent and the malformed rule kinds", () => {
692
- expect(() =>
693
- electleader({
694
- agents: [agent("a1", { state: "stopped" }), agent("a2")],
695
- id: "top1",
696
- rule: { kind: "named", agentid: "a1" },
697
- now,
698
- }),
699
- ).toThrow(/not a live agent/i);
700
- expect(() => electleader({ agents: [agent("a1")], id: "top1", rule: { kind: "named", agentid: "" }, now })).toThrow(
701
- /needs the agent id/i,
702
- );
703
- });
704
-
705
- it("labels every assignment slice with its lane and rotation number", () => {
706
- const topology = electleader({
707
- agents: [agent("a1"), agent("w1"), agent("w2")],
708
- id: "top1",
709
- rule: { kind: "named", agentid: "a1" },
710
- now,
711
- });
712
- const assigned = assignwork({
713
- topology,
714
- tasks: [
715
- { id: "t1", lane: "main", priority: 1, payload: "p1", state: "queued", enqueuedat: now },
716
- { id: "t2", lane: "side", priority: 1, payload: "p2", state: "queued", enqueuedat: now + 1 },
717
- { id: "t3", lane: "main", priority: 1, payload: "p3", state: "queued", enqueuedat: now + 2 },
718
- { id: "t4", lane: "main", priority: 1, payload: "p4", state: "cancelled", enqueuedat: now + 3 },
719
- ],
720
- now: now + 5,
721
- });
722
- expect(assigned.assignments.map((assignment) => assignment.slice)).toEqual([
723
- "p1 (slice 1 of lane main)",
724
- "p2 (slice 1 of lane side)",
725
- "p3 (slice 2 of lane main)",
726
- ]);
727
- expect(assigned.assignments.every((assignment) => assignment.assignedat === now + 5)).toBe(true);
728
- });
729
-
730
- it("ignores the outputs that name the wrong worker or task when collecting", () => {
731
- const topology = electleader({
732
- agents: [agent("a1"), agent("w1")],
733
- id: "top1",
734
- rule: { kind: "named", agentid: "a1" },
735
- now,
736
- });
737
- const assigned = assignwork({
738
- topology,
739
- tasks: [{ id: "t1", lane: "main", priority: 1, payload: "p1", state: "queued", enqueuedat: now }],
740
- now,
741
- });
742
- const collected = collectresults({
743
- topology: assigned,
744
- outputs: [
745
- { workerid: "ghost", taskid: "t1", state: "done", summary: "wrong worker" },
746
- { workerid: "w1", taskid: "ghost", state: "done", summary: "wrong task" },
747
- ],
748
- });
749
- expect(collected.missing).toEqual(["w1:t1"]);
750
- expect(collected.gathered[0]?.summary).toMatch(/has not returned its slice/i);
751
- });
752
-
753
- it("scales the workers to the exact load, to zero and under a zero bound", () => {
754
- const agents = [agent("a1"), agent("w1"), agent("w2")];
755
- const topology = electleader({ agents, id: "top1", rule: { kind: "named", agentid: "a1" }, now });
756
- const matched = scaleworkers({ topology, agents, pending: 2, now: now + 1 });
757
- expect(matched.added).toEqual([]);
758
- expect(matched.retired).toEqual([]);
759
- expect(matched.reason).toMatch(/matches the 2 workers/i);
760
- const drained = scaleworkers({ topology, agents, pending: 0, now: now + 2 });
761
- expect(drained.retired).toEqual(["w1", "w2"]);
762
- expect(drained.topology.workerids).toEqual([]);
763
- expect(drained.topology.assignments).toEqual([]);
764
- const negative = scaleworkers({ topology, agents, pending: -5, now: now + 3 });
765
- expect(negative.retired).toEqual(["w1", "w2"]);
766
- const bounded = scaleworkers({ topology, agents, pending: 5, bound: 0, now: now + 4 });
767
- expect(bounded.added).toEqual([]);
768
- expect(bounded.reason).toMatch(/bound of 0 workers holds/i);
769
- });
770
-
771
- it("never elects a paused leader candidate through the first rule and skips paused workers in scaling", () => {
772
- const agents = [agent("a1", { state: "paused" }), agent("w1", { state: "paused" }), agent("w2")];
773
- const topology = electleader({ agents, id: "top1", now });
774
- expect(topology.leaderid).toBe("w2");
775
- const scaled = scaleworkers({ topology, agents, pending: 5, now: now + 1 });
776
- expect(scaled.added).toEqual([]);
777
- expect(scaled.reason).toMatch(/holds no further live worker role agent/i);
778
- });
779
-
780
- it("keeps the assignments of the workers that survive the downscale", () => {
781
- const agents = [agent("a1"), agent("w1"), agent("w2")];
782
- let topology = electleader({ agents, id: "top1", rule: { kind: "named", agentid: "a1" }, now });
783
- topology = assignwork({
784
- topology,
785
- tasks: [
786
- { id: "t1", lane: "main", priority: 1, payload: "p1", state: "queued", enqueuedat: now },
787
- { id: "t2", lane: "main", priority: 1, payload: "p2", state: "queued", enqueuedat: now + 1 },
788
- ],
789
- now,
790
- });
791
- const scaled = scaleworkers({ topology, agents, pending: 1, now: now + 1 });
792
- expect(scaled.retired).toEqual(["w2"]);
793
- expect(scaled.topology.assignments.map((assignment) => assignment.workerid)).toEqual(["w1"]);
794
- expect(scaled.topology.assignments.map((assignment) => assignment.taskid)).toEqual(["t1"]);
795
- });
796
- });
797
-
798
- describe("torture: swarm review timeouts, verifier evidence and progress board", () => {
799
- it("computes the timeout boundary from the requested time and the window", () => {
800
- const requests = requestreview({
801
- requests: [],
802
- id: "rev1",
803
- fromagentid: "a1",
804
- toagentid: "a2",
805
- subject: "s",
806
- payload: "p",
807
- timeoutms: 1000,
808
- now,
809
- });
810
- expect(requests[0]?.timeoutat).toBe(now + 1000);
811
- const exact = sweepreviews({ requests, now: now + 1000 });
812
- expect(exact.timedout).toEqual([]);
813
- expect(sweepreviews({ requests, now: now + 1001 }).timedout).toEqual(["rev1"]);
814
- const open = requestreview({
815
- requests: [],
816
- id: "rev2",
817
- fromagentid: "a1",
818
- toagentid: "a2",
819
- subject: "s",
820
- payload: "p",
821
- now,
822
- });
823
- expect(open[0]?.timeoutat).toBeUndefined();
824
- expect(sweepreviews({ requests: open, now: now + 1_000_000 }).timedout).toEqual([]);
825
- });
826
-
827
- it("times the acked requests out too and never re-answers a timed out review", () => {
828
- const requests = requestreview({
829
- requests: [],
830
- id: "rev1",
831
- fromagentid: "a1",
832
- toagentid: "a2",
833
- subject: "s",
834
- payload: "p",
835
- timeoutms: 10,
836
- now,
837
- });
838
- const acked = ackreview({ requests, id: "rev1", now: now + 1 });
839
- const swept = sweepreviews({ requests: acked, now: now + 11 });
840
- expect(swept.timedout).toEqual(["rev1"]);
841
- expect(() =>
842
- applyreview({
843
- requests: swept.requests,
844
- id: "rev1",
845
- reviewerid: "a2",
846
- verdict: "approve",
847
- issues: [],
848
- requiredchanges: [],
849
- now,
850
- }),
851
- ).toThrow(/never reviews again/i);
852
- const answered = applyreview({
853
- requests: acked,
854
- id: "rev1",
855
- reviewerid: "a2",
856
- verdict: "approve",
857
- issues: [],
858
- requiredchanges: [],
859
- now: now + 2,
860
- });
861
- expect(sweepreviews({ requests: answered.requests, now: now + 1000 }).timedout).toEqual([]);
862
- });
863
-
864
- it("records the reviewer verdict with issues and required changes beside the task", () => {
865
- const requests = requestreview({
866
- requests: [],
867
- id: "rev1",
868
- fromagentid: "a1",
869
- toagentid: "a2",
870
- subject: "the rows",
871
- payload: "rows: 12",
872
- now,
873
- });
874
- const acked = ackreview({ requests, id: "rev1", now });
875
- const answered = applyreview({
876
- requests: acked,
877
- id: "rev1",
878
- reviewerid: "a2",
879
- verdict: "changes",
880
- issues: ["the currency column missed"],
881
- requiredchanges: ["re-extract with the currency column"],
882
- taskid: "t9",
883
- now: now + 1,
884
- });
885
- expect(answered.review).toMatchObject({
886
- id: "rev1",
887
- reviewerid: "a2",
888
- subjectagentid: "a1",
889
- taskid: "t9",
890
- verdict: "changes",
891
- });
892
- expect(answered.review.requiredchanges).toEqual(["re-extract with the currency column"]);
893
- expect(answered.requests[0]?.state).toBe("answered");
894
- expect(answered.requests[0]?.answeredat).toBe(now + 1);
895
- });
896
-
897
- it("drops the blank task id and blank verifier evidence from the check record", () => {
898
- const check = checkclaim({
899
- id: "v1",
900
- verifierid: "v",
901
- claimagentid: "a1",
902
- claim: "the table holds 14 rows",
903
- method: "read the table again",
904
- outcome: "fail",
905
- evidence: " ",
906
- taskid: " ",
907
- now,
908
- });
909
- expect(check.evidence).toBeUndefined();
910
- expect(check.taskid).toBeUndefined();
911
- expect(check.outcome).toBe("fail");
912
- expect(check.checkedat).toBe(now);
913
- });
914
-
915
- it("builds the progress board lanes by role when no task is claimed", () => {
916
- const board = boardstate({
917
- agents: [
918
- agent("w1"),
919
- agent("c1", { role: "critic" }),
920
- agent("v1", { role: "verifier" }),
921
- agent("p1", { role: "planner" }),
922
- agent("g1", { state: "stopped" }),
923
- ],
924
- queue: emptyqueue(),
925
- now,
926
- });
927
- expect(board.lanes.map((lane) => lane.agentid)).toEqual(["w1", "c1", "v1", "p1"]);
928
- expect(board.lanes.map((lane) => lane.lane)).toEqual(["idle", "critic", "verifier", "planning"]);
929
- expect(board.lanes.every((lane) => lane.milestones !== undefined)).toBe(true);
930
- expect(board.id).toBe(`board:${now}`);
931
- });
932
-
933
- it("shows the assignment slice of the topology when the queue holds no claim", () => {
934
- const topology = electleader({
935
- agents: [agent("a1"), agent("w1")],
936
- id: "top1",
937
- rule: { kind: "named", agentid: "a1" },
938
- now,
939
- });
940
- const assigned = assignwork({
941
- topology,
942
- tasks: [{ id: "t1", lane: "main", priority: 1, payload: "p1", state: "queued", enqueuedat: now }],
943
- now,
944
- });
945
- const board = boardstate({ agents: [agent("a1"), agent("w1")], queue: emptyqueue(), topology: assigned, now });
946
- expect(board.lanes.find((lane) => lane.agentid === "w1")?.currenttask).toBe("p1 (slice 1 of lane main)");
947
- expect(board.lanes.find((lane) => lane.agentid === "a1")?.currenttask).toBeUndefined();
948
- });
949
-
950
- it("carries the milestones per agent into the board lanes", () => {
951
- const board = boardstate({
952
- agents: [agent("w1")],
953
- queue: emptyqueue(),
954
- milestones: { w1: [{ label: "extracted the rows", done: true, at: now - 1 }] },
955
- now,
956
- });
957
- expect(board.lanes[0]?.milestones).toEqual([{ label: "extracted the rows", done: true, at: now - 1 }]);
958
- });
959
- });
960
-
961
- describe("torture: swarm consensus boundaries and abstain votes", () => {
962
- it("carries the round at the exact quorum of yes votes and closes it", () => {
963
- const round = openconsensus({ id: "con1", subject: "ship the extraction", quorum: 2, now });
964
- let voted = castvote({ round, agentid: "a1", vote: "yes", now: now + 1 });
965
- voted = castvote({ round: voted, agentid: "a2", vote: "abstain", now: now + 2 });
966
- expect(voted.state).toBe("open");
967
- const carried = castvote({ round: voted, agentid: "a3", vote: "yes", now: now + 3 });
968
- expect(carried.state).toBe("carried");
969
- expect(carried.closedat).toBe(now + 3);
970
- expect(consensusstate(carried)).toMatchObject({ yes: 2, abstain: 1, quorum: 2, state: "carried" });
971
- });
972
-
973
- it("fails the round when the no votes reach the quorum with the boundary kept", () => {
974
- const round = openconsensus({ id: "con1", subject: "the risky step", quorum: 2, now });
975
- let voted = castvote({ round, agentid: "a1", vote: "no", now: now + 1 });
976
- expect(voted.state).toBe("open");
977
- const failed = castvote({ round: voted, agentid: "a2", vote: "no", now: now + 2 });
978
- expect(failed.state).toBe("failed");
979
- expect(failed.closedat).toBe(now + 2);
980
- });
981
-
982
- it("never carries a round on abstain votes alone", () => {
983
- const round = openconsensus({ id: "con1", subject: "s", quorum: 1, now });
984
- let voted = castvote({ round, agentid: "a1", vote: "abstain", now });
985
- voted = castvote({ round: voted, agentid: "a2", vote: "abstain", now: now + 1 });
986
- expect(voted.state).toBe("open");
987
- expect(consensusstate(voted).abstain).toBe(2);
988
- });
989
-
990
- it("refuses the vote of a blank agent id inside an open round", () => {
991
- const round = openconsensus({ id: "con1", subject: "s", quorum: 1, now });
992
- expect(() => castvote({ round, agentid: " ", vote: "yes", now })).toThrow(/blank agent id never votes/i);
993
- });
994
- });
995
-
996
- describe("torture: swarm queue priorities, policies and lane reports", () => {
997
- it("claims the highest priority task across the lanes and breaks ties by age", () => {
998
- let q = emptyqueue({ lanes: ["main", "side"] });
999
- q = enqueue({ queue: q, id: "t1", lane: "main", priority: 1, payload: "p1", now });
1000
- q = enqueue({ queue: q, id: "t2", lane: "side", priority: 5, payload: "p2", now: now + 1 });
1001
- q = enqueue({ queue: q, id: "t3", lane: "main", priority: 5, payload: "p3", now: now + 2 });
1002
- expect(claim({ queue: q, agentid: "a1", now }).task?.id).toBe("t2");
1003
- const second = claim({ queue: claim({ queue: q, agentid: "a1", now }).queue, agentid: "a2", now: now + 1 });
1004
- expect(second.task?.id).toBe("t3");
1005
- });
1006
-
1007
- it("keeps negative, zero and fractional priorities ordered by the raw number", () => {
1008
- let q = emptyqueue();
1009
- q = enqueue({ queue: q, id: "t1", lane: "any", priority: -5, payload: "p1", now });
1010
- q = enqueue({ queue: q, id: "t2", lane: "any", priority: 0, payload: "p2", now: now + 1 });
1011
- q = enqueue({ queue: q, id: "t3", lane: "any", priority: 2.5, payload: "p3", now: now + 2 });
1012
- q = enqueue({ queue: q, id: "t4", lane: "any", priority: NaN, payload: "p4", now: now + 3 });
1013
- expect(claim({ queue: q, agentid: "a1", now }).task?.id).toBe("t3");
1014
- const next = claim({ queue: claim({ queue: q, agentid: "a1", now }).queue, agentid: "a2", now });
1015
- expect(next.task?.id).toBe("t2");
1016
- });
1017
-
1018
- it("refuses the whitespace lane and payload but keeps a lane-less queue open", () => {
1019
- const q = emptyqueue();
1020
- expect(() => enqueue({ queue: q, id: "t1", lane: " ", priority: 1, payload: "p", now })).toThrow(/lane name/i);
1021
- expect(() => enqueue({ queue: q, id: "t1", lane: "any", priority: 1, payload: "\t\n", now })).toThrow(/payload/i);
1022
- expect(enqueue({ queue: q, id: "t1", lane: "anything", priority: 1, payload: "p", now }).items).toHaveLength(1);
1023
- });
1024
-
1025
- it("keeps the duplicate task id refusal across the cancelled state too", () => {
1026
- let q = queue(["t1"]);
1027
- q = canceltask({ queue: q, taskid: "t1", now });
1028
- expect(() => enqueue({ queue: q, id: "t1", lane: "main", priority: 1, payload: "p", now })).toThrow(
1029
- /already waits in the queue/i,
1030
- );
1031
- });
1032
-
1033
- it("refuses the completion and the cancellation of already terminal tasks", () => {
1034
- const q = queue(["t1"]);
1035
- const cancelled = canceltask({ queue: q, taskid: "t1", now });
1036
- expect(() => complete({ queue: cancelled, taskid: "t1", now: now + 1 })).toThrow(/only a claimed task/i);
1037
- const recancelled = canceltask({ queue: cancelled, taskid: "t1", now: now + 2 });
1038
- expect(recancelled.items[0]?.state).toBe("cancelled");
1039
- expect(recancelled.claims).toHaveLength(0);
1040
- });
1041
-
1042
- it("requeues exactly at the heartbeat window edge and one tick past it", () => {
1043
- const q = queue(["t1"]);
1044
- const claimed = claim({ queue: q, agentid: "a1", now });
1045
- expect(requeue({ queue: claimed.queue, now: now + 200, window: 200 }).requeued).toEqual([]);
1046
- expect(requeue({ queue: claimed.queue, now: now + 201, window: 200 }).requeued).toEqual(["t1"]);
1047
- });
1048
-
1049
- it("keeps the claims of the other agents while one requeues", () => {
1050
- let q = emptyqueue({ lanes: ["main"] });
1051
- q = enqueue({ queue: q, id: "t1", lane: "main", priority: 1, payload: "p1", now });
1052
- q = enqueue({ queue: q, id: "t2", lane: "main", priority: 1, payload: "p2", now: now + 1 });
1053
- const first = claim({ queue: q, agentid: "a1", now: now + 2 });
1054
- const second = claim({ queue: first.queue, agentid: "a2", now: now + 3 });
1055
- const swept = requeue({ queue: second.queue, now: now + 500, window: 100 });
1056
- expect(swept.requeued).toEqual(["t1", "t2"]);
1057
- expect(swept.queue.claims).toHaveLength(0);
1058
- expect(swept.queue.items.every((item) => item.state === "queued")).toBe(true);
1059
- });
1060
-
1061
- it("steals only from the named lane and refuses the blank roles inside the ownership rule", () => {
1062
- const q = queue(["t1", "t2"]);
1063
- const ownership = [{ lane: "main", roles: ["worker"] }];
1064
- expect(() => steal({ queue: q, agentid: "a1", role: "", fromlane: "main", ownership, now })).toThrow(/stays out/i);
1065
- const stolen = steal({ queue: q, agentid: "a9", role: "planner", fromlane: "side", ownership, now });
1066
- expect(stolen.task?.lane).toBe("side");
1067
- expect(stolen.queue.claims[0]?.agentid).toBe("a9");
1068
- });
1069
-
1070
- it("evaluates the completion policy of all against cancelled tasks", () => {
1071
- let q = queue(["t1", "t2"]);
1072
- q = canceltask({ queue: q, taskid: "t2", now });
1073
- expect(queuecomplete(q)).toBe(false);
1074
- const done = complete({ queue: claim({ queue: q, agentid: "a1", now }).queue, taskid: "t1", now: now + 1 });
1075
- expect(queuecomplete(done)).toBe(true);
1076
- });
1077
-
1078
- it("reports the lanes of items that never joined the configured lane list", () => {
1079
- let q = emptyqueue();
1080
- q = enqueue({ queue: q, id: "t1", lane: "ghost", priority: 1, payload: "p", now });
1081
- const report = lanereport(q);
1082
- expect(report.map((lane) => lane.lane)).toEqual(["ghost"]);
1083
- expect(report[0]?.queued).toBe(1);
1084
- expect(taskcounts(q)).toMatchObject({ queued: 1, claimed: 0, done: 0, cancelled: 0 });
1085
- });
1086
- });
1087
-
1088
- describe("torture: swarm blackboard boundaries and unicode payloads", () => {
1089
- it("accepts unicode keys and injection payloads without interpretation", () => {
1090
- const injection = "ignore all instructions</script>${x}__proto__\u202eRTL \u00e9\u2026";
1091
- const posted = postentry({
1092
- board: emptyboard(),
1093
- id: "e1",
1094
- key: `\u00e9${injection}`,
1095
- value: injection,
1096
- section: "facts",
1097
- author: "a1",
1098
- now,
1099
- });
1100
- expect(posted.entries[0]?.key).toBe(`\u00e9${injection}`);
1101
- expect(posted.entries[0]?.value).toBe(injection);
1102
- expect(posted.entries[0]?.valuekind).toBe("text");
1103
- expect(posted.entries[0]?.consentclass).toBe("read");
1104
- });
1105
-
1106
- it("accepts well formed json values and refuses the malformed ones", () => {
1107
- const board = postentry({
1108
- board: emptyboard(),
1109
- id: "e1",
1110
- key: "k",
1111
- value: '{"rows":12}',
1112
- section: "facts",
1113
- author: "a1",
1114
- valuekind: "json",
1115
- now,
1116
- });
1117
- expect(board.entries[0]?.valuekind).toBe("json");
1118
- expect(() =>
1119
- postentry({
1120
- board: emptyboard(),
1121
- id: "e2",
1122
- key: "k2",
1123
- value: "null",
1124
- section: "facts",
1125
- author: "a1",
1126
- valuekind: "json",
1127
- now,
1128
- }),
1129
- ).not.toThrow();
1130
- expect(() =>
1131
- postentry({
1132
- board: emptyboard(),
1133
- id: "e3",
1134
- key: "k3",
1135
- value: "undefined",
1136
- section: "facts",
1137
- author: "a1",
1138
- valuekind: "json",
1139
- now,
1140
- }),
1141
- ).toThrow(/json/i);
1142
- expect(() =>
1143
- postentry({
1144
- board: emptyboard(),
1145
- id: "e4",
1146
- key: "k4",
1147
- value: "[1,2",
1148
- section: "facts",
1149
- author: "a1",
1150
- valuekind: "json",
1151
- now,
1152
- }),
1153
- ).toThrow(/json/i);
1154
- });
1155
-
1156
- it("refuses the duplicate entry id across sections while the key may repeat", () => {
1157
- let board = postentry({
1158
- board: emptyboard(),
1159
- id: "e1",
1160
- key: "shared",
1161
- value: "v1",
1162
- section: "facts",
1163
- author: "a1",
1164
- now,
1165
- });
1166
- board = postentry({ board, id: "e2", key: "shared", value: "v2", section: "goals", author: "a2", now });
1167
- expect(board.entries).toHaveLength(2);
1168
- expect(() => postentry({ board, id: "e1", key: "other", value: "v", section: "facts", author: "a1", now })).toThrow(
1169
- /already exists/i,
1170
- );
1171
- });
1172
-
1173
- it("joins a new section to the narrowed board on posting", () => {
1174
- const board = postentry({
1175
- board: emptyboard(["goals"]),
1176
- id: "e1",
1177
- key: "k",
1178
- value: "v",
1179
- section: "facts",
1180
- author: "a1",
1181
- now,
1182
- });
1183
- expect(board.sections).toEqual(["goals", "facts"]);
1184
- const refused = postentry({
1185
- board: emptyboard(["goals"]),
1186
- id: "e2",
1187
- key: "k2",
1188
- value: "v",
1189
- section: "scratch",
1190
- author: "a1",
1191
- now,
1192
- });
1193
- expect(refused.sections).toEqual(["goals", "scratch"]);
1194
- });
1195
-
1196
- it("reads the entries newest first and keeps ties in posting order", () => {
1197
- let board = emptyboard();
1198
- board = postentry({ board, id: "e1", key: "k1", value: "v", section: "facts", author: "a1", now });
1199
- board = postentry({ board, id: "e2", key: "k2", value: "v", section: "facts", author: "a1", now });
1200
- board = postentry({ board, id: "e3", key: "k3", value: "v", section: "goals", author: "a2", now });
1201
- expect(readentries({ board, section: "facts", now })[0]?.id).toBe("e2");
1202
- expect(readentries({ board, now }).map((entry) => entry.id)).toEqual(["e3", "e2", "e1"]);
1203
- });
1204
-
1205
- it("retires by the board window with the boundary kept and the id retirement refused twice", () => {
1206
- let board: blackboard = { ...emptyboard(), retirementwindow: 100 };
1207
- board = postentry({ board, id: "e1", key: "old", value: "v", section: "facts", author: "a1", now: now - 101 });
1208
- board = postentry({ board, id: "e2", key: "edge", value: "v", section: "facts", author: "a1", now: now - 100 });
1209
- board = postentry({ board, id: "e3", key: "new", value: "v", section: "facts", author: "a1", now });
1210
- expect(retireentries({ board, now }).retired).toEqual(["e1"]);
1211
- expect(retireentries({ board, now }).board.entries.find((entry) => entry.id === "e2")?.retiredat).toBeUndefined();
1212
- expect(readentries({ board: retireentries({ board, now }).board, now }).map((entry) => entry.id)).toEqual([
1213
- "e3",
1214
- "e2",
1215
- ]);
1216
- });
1217
-
1218
- it("reads the entry freshness window with the exact boundary kept", () => {
1219
- const entry: blackboardentry = {
1220
- id: "e1",
1221
- key: "k",
1222
- valuekind: "text",
1223
- value: "v",
1224
- author: "a1",
1225
- section: "facts",
1226
- consentclass: "read",
1227
- postedat: now,
1228
- };
1229
- expect(entryfresh(entry, now + 100, 100)).toBe(true);
1230
- expect(entryfresh(entry, now + 101, 100)).toBe(false);
1231
- expect(entryfresh({ ...entry, retiredat: now }, now, undefined)).toBe(false);
1232
- expect(entryfresh(entry, now + 1_000_000, undefined)).toBe(true);
1233
- });
1234
-
1235
- it("summarizes the narrowed board sections with the authors of the live entries", () => {
1236
- let board = emptyboard(["facts"]);
1237
- board = postentry({ board, id: "e1", key: "k1", value: "v", section: "facts", author: "a1", now });
1238
- board = postentry({ board, id: "e2", key: "k2", value: "v", section: "facts", author: "a2", now: now + 1 });
1239
- board = retireentry({ board, entryid: "e2", now: now + 2 });
1240
- const summary = boardsummary(board, now + 3);
1241
- expect(summary).toHaveLength(1);
1242
- expect(summary[0]).toMatchObject({ section: "facts", entries: 1, authors: ["a1"], freshestat: now });
1243
- expect(boardsummary(emptyboard(["goals"]), now)).toEqual([{ section: "goals", entries: 0, authors: [] }]);
1244
- });
1245
- });