@wenathlan/devthink 2.0.13 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
@@ -0,0 +1,923 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ advancecursor,
4
+ advancestream,
5
+ artifactrecordof,
6
+ backpressure,
7
+ builddataset,
8
+ checksum,
9
+ chunkextractprogress,
10
+ chunkextracttaskof,
11
+ chunkextractwindow,
12
+ chunkplan,
13
+ deduperows,
14
+ exportartifact,
15
+ exportcontent,
16
+ extractbatchof,
17
+ gridpreview,
18
+ interpolate,
19
+ loopstep,
20
+ loopvariables,
21
+ mergechunkwindows,
22
+ mergetaskrules,
23
+ newextractsession,
24
+ newstream,
25
+ openchunkextractcursor,
26
+ pipelinestate,
27
+ provenancefor,
28
+ pipelinegridpreview,
29
+ provlog,
30
+ provlogquery,
31
+ remainingpages,
32
+ resumechunkextract,
33
+ resumeextract,
34
+ retainedexports,
35
+ samplerows,
36
+ sheetpayload,
37
+ sortrows,
38
+ sourcestamp,
39
+ streamdisk,
40
+ streamfrom,
41
+ streamparsechunkof,
42
+ streamparsememorybound,
43
+ streamparseobservations,
44
+ streamparsewindows,
45
+ streamparsetaskof,
46
+ transformvalues,
47
+ validatesetreamparsechunk,
48
+ } from "../data.js";
49
+ import type {
50
+ columnspec,
51
+ dataset,
52
+ extractpipeline,
53
+ extractrow,
54
+ streamcursor,
55
+ streamstate,
56
+ transformrule,
57
+ } from "../types.js";
58
+
59
+ const now = 1_800_000_000_000;
60
+
61
+ /** Builds one reviewed pipeline fixture of the pricing extraction. */
62
+ function pipeline(over: Partial<extractpipeline> = {}): extractpipeline {
63
+ return {
64
+ id: "p1",
65
+ runid: "run1",
66
+ name: "Pricing rows",
67
+ source: "table.pricing",
68
+ steps: ["s1", "s2"],
69
+ transforms: [],
70
+ sink: "csv",
71
+ state: "running",
72
+ origin: "https://example.com",
73
+ createdat: now,
74
+ updatedat: now,
75
+ ...over,
76
+ };
77
+ }
78
+
79
+ /** Builds one extracted row fixture with its key and values. */
80
+ function row(key: string, values: Record<string, string>, over: Partial<extractrow> = {}): extractrow {
81
+ return { key, values, stamps: [], provenance: [], ...over };
82
+ }
83
+
84
+ /** Builds one dataset fixture with the given columns and rows. */
85
+ function datasetof(columns: columnspec[], rows: Array<Record<string, string>>): dataset {
86
+ return builddataset("d1", "Pricing", { columns, rows }, now);
87
+ }
88
+
89
+ const textcolumn = (key: string, label = key): columnspec => ({ key, label, kind: "text", normalized: "none" });
90
+ const numbercolumn = (key: string, label = key): columnspec => ({ key, label, kind: "number", normalized: "none" });
91
+
92
+ describe("torture: dataset records, checksums and artifact records", () => {
93
+ it("checksums deterministically over empty, ascii, unicode and null byte payloads", () => {
94
+ expect(checksum("")).toBe(checksum(""));
95
+ expect(checksum("devthink")).toMatch(/^fnv1a-[0-9a-f]+$/);
96
+ expect(checksum("a")).not.toBe(checksum("b"));
97
+ expect(checksum("éé")).not.toBe(checksum("ée"));
98
+ expect(checksum("x\0y")).toMatch(/^fnv1a-/);
99
+ expect(checksum("𝕏")).toMatch(/^fnv1a-/);
100
+ });
101
+
102
+ it("builds datasets with an empty name falling back to the id and passing the grid through untouched", () => {
103
+ const bare = builddataset("d9", "", { columns: [textcolumn("a")], rows: [{ a: "1" }] }, now);
104
+ expect(bare.name).toBe("d9");
105
+ expect(bare.sources).toEqual([]);
106
+ expect(bare.at).toBe(now);
107
+ expect(bare.rows).toEqual([{ a: "1" }]);
108
+ });
109
+
110
+ it("exports artifacts with format extensions, row counts and checksums that track the content", () => {
111
+ const ds = datasetof([textcolumn("a")], [{ a: "1" }, { a: "2" }]);
112
+ const csv = exportartifact("art1", ds, "csv", "s1", "a,b\n1,2", now);
113
+ expect(csv.name).toBe("Pricing.csv");
114
+ expect(csv.rowcount).toBe(2);
115
+ expect(csv.checksum).toBe(checksum("a,b\n1,2"));
116
+ expect(exportartifact("art2", ds, "excel", "s1", "<workbook/>", now).name).toBe("Pricing.xml");
117
+ expect(exportartifact("art3", ds, "json", "s1", "{}", now).name).toBe("Pricing.json");
118
+ expect(artifactrecordof(csv)).toEqual({ id: "art1", kind: "csv", name: "Pricing.csv", stepid: "s1", at: now });
119
+ });
120
+
121
+ it("renders the csv, json and excel export formats with escaping and a custom delimiter", () => {
122
+ const columns = [textcolumn("note", "Note"), numbercolumn("qty", "Qty")];
123
+ const rows = [{ note: 'has "quotes", commas\nand newlines', qty: "2" }];
124
+ const csv = exportcontent(datasetof(columns, rows), "csv");
125
+ expect(csv).toContain('has ""quotes"", commas\nand newlines');
126
+ const semicolon = exportcontent(datasetof(columns, rows), "csv", ";");
127
+ expect(semicolon.split("\n")[0]).toBe("Note;Qty");
128
+ const json = exportcontent(datasetof(columns, rows), "json");
129
+ expect(JSON.parse(json).rows[0].qty).toBe("2");
130
+ const excel = exportcontent(datasetof(columns, rows), "excel");
131
+ expect(excel).toContain("&quot;quotes&quot;");
132
+ expect(excel).not.toContain("<script");
133
+ });
134
+
135
+ it("escapes xml hostile cell payloads inside the excel export", () => {
136
+ const excel = exportcontent(datasetof([textcolumn("cell")], [{ cell: "<script>alert(1)</script>&amp;" }]), "excel");
137
+ expect(excel).toContain("&lt;script&gt;");
138
+ expect(excel).not.toContain("<script>alert");
139
+ });
140
+
141
+ // REPORTED: shared-module bug — see worklog 2-c: page.ts tocsv ships formula-leading cells (= + @ tab/CR) raw through exportcontent, so the datacommand csv export carries spreadsheet formula payloads; page.ts is outside this agent's owned files.
142
+ it.skip("neutralizes spreadsheet formula payloads in the csv export", () => {
143
+ const csv = exportcontent(
144
+ datasetof([textcolumn("cell")], [{ cell: "=1+1" }, { cell: "+cmd|' /C calc'!A0" }, { cell: "@SUM(A1:A9)" }]),
145
+ "csv",
146
+ );
147
+ expect(csv).not.toMatch(/^[=+@]/m);
148
+ });
149
+
150
+ it("keeps formula cells as inert json values", () => {
151
+ const json = exportcontent(datasetof([textcolumn("cell")], [{ cell: "=1+1" }]), "json");
152
+ expect(JSON.parse(json).rows[0].cell).toBe("=1+1");
153
+ });
154
+
155
+ it("plans chunk boundaries for zero, exact, fractional, negative and huge chunk sizes", () => {
156
+ expect(chunkplan(0, 5)).toEqual([{ index: 0, from: 0, to: 0 }]);
157
+ expect(chunkplan(10, 3)).toHaveLength(4);
158
+ expect(chunkplan(10, 3).at(-1)).toEqual({ index: 3, from: 9, to: 10 });
159
+ expect(chunkplan(10, 0.9)).toHaveLength(10);
160
+ expect(chunkplan(10, -5)).toHaveLength(10);
161
+ expect(chunkplan(10, 10)).toEqual([{ index: 0, from: 0, to: 10 }]);
162
+ expect(chunkplan(10, 1_000_000)).toEqual([{ index: 0, from: 0, to: 10 }]);
163
+ expect(chunkplan(7, 2).every((chunk) => chunk.to - chunk.from <= 2)).toBe(true);
164
+ });
165
+
166
+ it("gates backpressure at the in flight budget of one", () => {
167
+ expect(backpressure(0, 0)).toBe(false);
168
+ expect(backpressure(1, 0)).toBe(true);
169
+ expect(backpressure(5, 5)).toBe(false);
170
+ expect(backpressure(5, 4)).toBe(true);
171
+ });
172
+
173
+ it("advances stream state and reads the first unwritten row from fresh, done and past the end states", () => {
174
+ const stream: streamstate = newstream(datasetof([textcolumn("a")], [{ a: "1" }]), 3, now);
175
+ expect(stream).toEqual({ datasetid: "d1", name: "Pricing", chunk: 0, chunks: 3, written: 0, at: now });
176
+ const advanced = advancestream(stream, { index: 1, to: 4 }, now + 10, false);
177
+ expect(advanced).toEqual({ datasetid: "d1", name: "Pricing", chunk: 2, chunks: 3, written: 4, at: now + 10 });
178
+ expect(advanced.done).toBeUndefined();
179
+ const finished = advancestream(stream, { index: 2, to: 10 }, now + 20, true);
180
+ expect(finished.done).toBe(true);
181
+ expect(streamfrom(undefined, 5)).toBe(0);
182
+ expect(streamfrom(finished, 5)).toBe(0);
183
+ expect(streamfrom(advanced, 2)).toBe(2);
184
+ expect(streamfrom({ ...advanced, written: 7 }, 5)).toBe(5);
185
+ });
186
+ });
187
+
188
+ describe("torture: extraction sessions, provenance and retention", () => {
189
+ it("walks extraction sessions page by page until the planned cursor completes", () => {
190
+ const session = newextractsession("e1", "d1", "Pricing", "https://example.com", "?page=2", 2, now);
191
+ expect(session).toMatchObject({ id: "e1", datasetid: "d1", cursor: 0, rows: 0, pages: [] });
192
+ const one = advancecursor(session, "?page=2", 20, now + 10, false);
193
+ expect(one.cursor).toBe(1);
194
+ expect(one.rows).toBe(20);
195
+ expect(one.done).toBeUndefined();
196
+ const two = advancecursor(one, "?page=3", 10, now + 20, false);
197
+ expect(two.done).toBe(true);
198
+ expect(two.pages).toEqual(["?page=2", "?page=3"]);
199
+ expect(remainingpages(two, 5)).toBe(0);
200
+ expect(remainingpages(one, 5)).toBe(4);
201
+ expect(remainingpages(one, 2)).toBe(1);
202
+ expect(remainingpages(newextractsession("e2", "d1", "Pricing", "https://example.com", "", 5, now), 2)).toBe(5);
203
+ });
204
+
205
+ it("stamps provenance with one based row ranges and applies retention as a head slice", () => {
206
+ const provenance = provenancefor(
207
+ { id: "art1", name: "Pricing.csv", rowcount: 42, checksum: "fnv1a-1" },
208
+ "https://example.com",
209
+ "s1",
210
+ now,
211
+ );
212
+ expect(provenance).toEqual({
213
+ artifact: "art1",
214
+ name: "Pricing.csv",
215
+ url: "https://example.com",
216
+ stepid: "s1",
217
+ rowstart: 1,
218
+ rowend: 42,
219
+ checksum: "fnv1a-1",
220
+ at: now,
221
+ });
222
+ expect(provenancefor({ id: "a", name: "n", rowcount: 0, checksum: "c" }, "u", "s", now).rowstart).toBe(0);
223
+ expect(retainedexports([1, 2, 3], undefined)).toEqual([1, 2, 3]);
224
+ expect(retainedexports([1, 2, 3], 0)).toEqual([]);
225
+ expect(retainedexports([1, 2, 3], 2)).toEqual([1, 2]);
226
+ expect(retainedexports([1, 2, 3], 99)).toEqual([1, 2, 3]);
227
+ expect(retainedexports([1, 2, 3], -5)).toEqual([]);
228
+ });
229
+ });
230
+
231
+ describe("torture: row interpolation and loop variables", () => {
232
+ it("interpolates {{column}} tokens with trim, missing keys and repeated tokens", () => {
233
+ expect(interpolate("{{name}} costs {{ price }}", { name: "Devthink", price: "1299.50" })).toBe(
234
+ "Devthink costs 1299.50",
235
+ );
236
+ expect(interpolate("{{missing}}", { name: "x" })).toBe("");
237
+ expect(interpolate("{{a}}{{a}}{{a}}", { a: "x" })).toBe("xxx");
238
+ expect(interpolate("no tokens", { a: "x" })).toBe("no tokens");
239
+ expect(interpolate("{{ name", { name: "x" })).toBe("{{ name");
240
+ expect(interpolate("{{}}", { "": "empty key" })).toBe("{{}}");
241
+ });
242
+
243
+ it("refuses to interpolate inherited properties so a crafted token never reads object internals", () => {
244
+ expect(interpolate("{{constructor}}", {} as Record<string, string>)).toBe("");
245
+ expect(interpolate("{{toString}}", {} as Record<string, string>)).toBe("");
246
+ expect(interpolate("{{__proto__}}", {} as Record<string, string>)).toBe("");
247
+ expect(interpolate("{{ hasOwnProperty }}", {} as Record<string, string>)).toBe("");
248
+ });
249
+
250
+ it("substitutes row variables into the loop step target, value and options", () => {
251
+ const step = {
252
+ id: "s1",
253
+ kind: "click" as const,
254
+ target: "#{{name}}",
255
+ value: "{{price}}",
256
+ options: '{"row":"{{name}}"}',
257
+ summary: "",
258
+ risk: "interaction" as const,
259
+ };
260
+ const substituted = loopstep(step, { name: "buy", price: "12" });
261
+ expect(substituted.target).toBe("#buy");
262
+ expect(substituted.value).toBe("12");
263
+ expect(JSON.parse(substituted.options ?? "{}")).toEqual({ row: "buy" });
264
+ const untouched = loopstep({ id: "s2", kind: "wait" as const, summary: "", risk: "read" as const }, { name: "x" });
265
+ expect(untouched.target).toBeUndefined();
266
+ expect(untouched.options).toBeUndefined();
267
+ const variables = loopvariables({ name: "x", nested: "y" });
268
+ expect(variables).toEqual({ name: "x", nested: "y" });
269
+ variables.name = "changed";
270
+ expect(loopvariables({ name: "x" }).name).toBe("x");
271
+ });
272
+ });
273
+
274
+ describe("torture: grid preview, sorting and task rules", () => {
275
+ it("previews the grid with zero, negative, fractional and huge sample bounds", () => {
276
+ const ds = datasetof([textcolumn("a")], [{ a: "1" }, { a: "2" }, { a: "3" }]);
277
+ expect(gridpreview(ds, 0).sample).toEqual([]);
278
+ expect(gridpreview(ds, -3).sample).toEqual([]);
279
+ expect(gridpreview(ds, 2.9).sample).toHaveLength(2);
280
+ expect(gridpreview(ds, 1_000_000).sample).toHaveLength(3);
281
+ expect(gridpreview(ds, 1).columns).toEqual(["a"]);
282
+ expect(gridpreview(ds, 1).rows).toBe(3);
283
+ });
284
+
285
+ it("sorts numerically when both cells parse and falls back to collation otherwise", () => {
286
+ const rows = [{ n: "10" }, { n: "9" }, { n: "2.5" }, { n: "-1" }];
287
+ expect(sortrows(rows, "n", "asc").map((entry) => entry.n)).toEqual(["-1", "2.5", "9", "10"]);
288
+ expect(sortrows(rows, "n", "desc").map((entry) => entry.n)).toEqual(["10", "9", "2.5", "-1"]);
289
+ const textrows = [{ n: "beta" }, { n: "Alpha" }, { n: "" }];
290
+ expect(sortrows(textrows, "n", "asc").map((entry) => entry.n)).toEqual(["", "Alpha", "beta"]);
291
+ const missing = sortrows([{ n: "1" }, { m: "2" }], "ghost", "asc");
292
+ expect(missing.map((entry) => entry.n ?? "")).toEqual(["1", ""]);
293
+ expect(sortrows([], "n", "asc")).toEqual([]);
294
+ });
295
+
296
+ it("merges task rules keeping the stored list when the new list is empty", () => {
297
+ const stored = {
298
+ taskid: "t1",
299
+ transforms: [{ expression: "", sources: [], target: "a", field: "a", operation: "trim" as const }],
300
+ dedupekeys: ["a"],
301
+ };
302
+ const merged = mergetaskrules(stored, "t1", [], ["b"], now);
303
+ expect(merged.transforms).toEqual(stored.transforms);
304
+ expect(merged.dedupekeys).toEqual(["b"]);
305
+ const fresh = mergetaskrules(undefined, "t2", [], [], now);
306
+ expect(fresh).toEqual({ taskid: "t2", transforms: [], dedupekeys: [], at: now });
307
+ const sheet = sheetpayload(datasetof([textcolumn("a")], [{ a: "1" }]), "Inventory");
308
+ expect(sheet).toEqual({ sheet: "Inventory", columns: ["a"], rows: [{ a: "1" }] });
309
+ });
310
+ });
311
+
312
+ describe("torture: streamdisk, resume and transform passes", () => {
313
+ it("streams empty extracts without one write and survives a cursor pointing past the end", () => {
314
+ const writes: number[] = [];
315
+ const empty = streamdisk({
316
+ pipeline: pipeline(),
317
+ rows: [],
318
+ chunk: 5,
319
+ write: (chunk) => {
320
+ writes.push(chunk.length);
321
+ return 1;
322
+ },
323
+ now,
324
+ });
325
+ expect(writes).toEqual([]);
326
+ expect(empty.rowswritten).toBe(0);
327
+ expect(empty.chunks).toBe(0);
328
+ expect(empty.cursor).toEqual({ pipelineid: "p1", offset: 0, chunk: 0, updatedat: now });
329
+ const past = streamdisk({
330
+ pipeline: pipeline(),
331
+ rows: [row("r1", { a: "1" })],
332
+ cursor: { pipelineid: "p1", offset: 100, chunk: 9, updatedat: now },
333
+ chunk: 5,
334
+ write: () => 1,
335
+ now,
336
+ });
337
+ expect(past.rowswritten).toBe(0);
338
+ expect(past.cursor.offset).toBe(100);
339
+ });
340
+
341
+ it("refuses invalid chunk sizes and propagates a sink error mid stream", () => {
342
+ expect(() =>
343
+ streamdisk({ pipeline: pipeline(), rows: [row("r1", { a: "1" })], chunk: 0, write: () => 1, now }),
344
+ ).toThrow(/positive whole number/);
345
+ expect(() =>
346
+ streamdisk({ pipeline: pipeline(), rows: [row("r1", { a: "1" })], chunk: -2, write: () => 1, now }),
347
+ ).toThrow(/positive whole number/);
348
+ expect(() =>
349
+ streamdisk({ pipeline: pipeline(), rows: [row("r1", { a: "1" })], chunk: 1.5, write: () => 1, now }),
350
+ ).toThrow(/positive whole number/);
351
+ expect(() => streamdisk({ pipeline: pipeline(), rows: [], write: () => 1, now })).not.toThrow();
352
+ expect(() => streamdisk({ pipeline: pipeline({ id: " " }), rows: [], write: () => 1, now })).toThrow(
353
+ /needs its pipeline/,
354
+ );
355
+ let wrote = 0;
356
+ expect(() =>
357
+ streamdisk({
358
+ pipeline: pipeline(),
359
+ rows: [row("r1", { a: "1" }), row("r2", { a: "2" })],
360
+ chunk: 1,
361
+ write: () => {
362
+ wrote += 1;
363
+ if (wrote === 2) throw new Error("sink exploded");
364
+ return 1;
365
+ },
366
+ now,
367
+ }),
368
+ ).toThrow(/sink exploded/);
369
+ expect(wrote).toBe(2);
370
+ });
371
+
372
+ it("resumes with a cursor at the end, all keys persisted and refuses finished pipelines and foreign cursors", () => {
373
+ const rows = [row("r1", { a: "1" }), row("r2", { a: "2" })];
374
+ const atend = resumeextract({
375
+ pipeline: pipeline({ state: "paused" }),
376
+ rows,
377
+ cursor: { pipelineid: "p1", offset: 2, chunk: 2, updatedat: now },
378
+ now,
379
+ });
380
+ expect(atend.rows).toEqual([]);
381
+ expect(atend.skipped).toBe(2);
382
+ const allpersisted = resumeextract({
383
+ pipeline: pipeline({ state: "paused" }),
384
+ rows,
385
+ persistedkeys: ["r1", "r2"],
386
+ now,
387
+ });
388
+ expect(allpersisted.rows).toEqual([]);
389
+ expect(allpersisted.skipped).toBe(2);
390
+ expect(() => resumeextract({ pipeline: pipeline({ state: "finished" }), rows, now })).toThrow(/already finished/);
391
+ expect(() =>
392
+ resumeextract({
393
+ pipeline: pipeline(),
394
+ rows,
395
+ cursor: { pipelineid: "p9", offset: 0, chunk: 0, updatedat: now },
396
+ now,
397
+ }),
398
+ ).toThrow(/another pipeline/);
399
+ expect(() => resumeextract({ pipeline: pipeline({ id: " " }), rows, now })).toThrow(/names its pipeline/);
400
+ const partial = resumeextract({
401
+ pipeline: pipeline({ state: "paused" }),
402
+ rows,
403
+ cursor: { pipelineid: "p1", offset: 1, chunk: 1, updatedat: now },
404
+ persistedkeys: ["r2"],
405
+ now,
406
+ });
407
+ expect(partial.rows).toEqual([]);
408
+ expect(partial.skipped).toBe(2);
409
+ });
410
+
411
+ it("keeps every row through number, case, trim and date transforms with refusals reported", () => {
412
+ const rules: transformrule[] = [
413
+ { expression: "", sources: [], target: "price", field: "price", operation: "number" },
414
+ { expression: "", sources: [], target: "name", field: "name", operation: "case" },
415
+ { expression: "", sources: [], target: "seen", field: "seen", operation: "date" },
416
+ {
417
+ expression: "",
418
+ sources: [],
419
+ target: "mystery",
420
+ field: "mystery",
421
+ operation: "hex",
422
+ } as unknown as transformrule,
423
+ ];
424
+ const rows = [
425
+ row("r1", { price: "$ 1,299.50", name: " dev think ", seen: "March 3, 2026", mystery: "x" }),
426
+ row("r2", { price: "free", name: "plain", seen: "no date", mystery: "y" }),
427
+ ];
428
+ const result = transformvalues({ rows, rules });
429
+ expect(result.rows).toHaveLength(2);
430
+ expect(result.rows[0]?.values.price).toBe("1299.50");
431
+ expect(result.rows[0]?.rawvalues?.price).toBe("$ 1,299.50");
432
+ expect(result.rows[0]?.values.name).toBe(" DEV THINK ");
433
+ expect(result.rows[1]?.values.price).toBe("");
434
+ expect(result.rows[1]?.rawvalues?.price).toBe("free");
435
+ expect(result.rows[1]?.values.seen).toBe("no date");
436
+ expect(result.rows[1]?.rawvalues?.seen).toBeUndefined();
437
+ expect(result.refusals.join(" ")).toContain("not a reviewed transform");
438
+ expect(result.refusals.join(" ")).toContain("parses as no date");
439
+ expect(result.refusals).toHaveLength(3);
440
+ });
441
+
442
+ it("falls back to the rule target when the field name is absent", () => {
443
+ const result = transformvalues({
444
+ rows: [row("r1", { sku: " a " })],
445
+ rules: [{ expression: "", sources: [], target: "sku", operation: "trim" } as unknown as transformrule],
446
+ });
447
+ expect(result.rows[0]?.values.sku).toBe("a");
448
+ expect(result.rows[0]?.rawvalues?.sku).toBe(" a ");
449
+ expect(result.applied).toBe(1);
450
+ });
451
+ });
452
+
453
+ describe("torture: dedupe normalization collisions and sampler bounds", () => {
454
+ it("keeps distinct rows apart when a cell value embeds the join separator", () => {
455
+ const rows = [row("r1", { a: "x|y", b: "1" }), row("r2", { a: "x", b: "y|1" }), row("r3", { a: "x", b: "1" })];
456
+ const deduped = deduperows({ rows, key: { columns: ["a", "b"], normalization: "none" } });
457
+ expect(deduped.kept.map((entry) => entry.key)).toEqual(["r1", "r2", "r3"]);
458
+ expect(deduped.dropped).toBe(0);
459
+ });
460
+
461
+ it("folds whitespace and case exactly as the normalization names", () => {
462
+ const rows = [
463
+ row("r1", { a: "Devthink" }),
464
+ row("r2", { a: " devthink " }),
465
+ row("r3", { a: "DEVTHINK" }),
466
+ row("r4", { a: "Other" }),
467
+ ];
468
+ const both = deduperows({ rows, key: { columns: ["a"], normalization: "whitespace and case" } });
469
+ expect(both.kept.map((entry) => entry.key)).toEqual(["r1", "r4"]);
470
+ const whitespace = deduperows({ rows, key: { columns: ["a"], normalization: "whitespace" } });
471
+ expect(whitespace.kept.map((entry) => entry.key)).toEqual(["r1", "r2", "r3", "r4"]);
472
+ });
473
+
474
+ it("refuses empty and whitespace only key columns while trimming the configured ones", () => {
475
+ const rows = [row("r1", { a: "1" })];
476
+ expect(() => deduperows({ rows, key: { columns: [], normalization: "none" } })).toThrow(/names its columns/);
477
+ expect(() => deduperows({ rows, key: { columns: [" "], normalization: "none" } })).toThrow(/names its columns/);
478
+ expect(deduperows({ rows, key: { columns: [" a "], normalization: "none" } }).kept).toHaveLength(1);
479
+ expect(deduperows({ rows: [], key: { columns: ["a"], normalization: "none" } })).toMatchObject({
480
+ kept: [],
481
+ dropped: 0,
482
+ });
483
+ });
484
+
485
+ it("samples with one row, huge bounds, deterministic seeds and empty extracts", () => {
486
+ const rows = Array.from({ length: 12 }, (_, index) => row(`r${index + 1}`, { a: String(index) }));
487
+ expect(samplerows({ rows, policy: { rows: 1, strategy: "first" } }).rows.map((entry) => entry.key)).toEqual(["r1"]);
488
+ expect(samplerows({ rows, policy: { rows: 999, strategy: "first" } }).rows).toHaveLength(12);
489
+ expect(samplerows({ rows: [], policy: { rows: 5, strategy: "stratified" } }).rows).toEqual([]);
490
+ expect(samplerows({ rows: [], policy: { rows: 5, strategy: "random" } }).rows).toEqual([]);
491
+ const one = samplerows({ rows, policy: { rows: 5, strategy: "random" }, seed: 7 });
492
+ const two = samplerows({ rows, policy: { rows: 5, strategy: "random" }, seed: 7 });
493
+ expect(one.rows.map((entry) => entry.key)).toEqual(two.rows.map((entry) => entry.key));
494
+ expect(new Set(one.rows.map((entry) => entry.key)).size).toBe(5);
495
+ const stratified = samplerows({ rows, policy: { rows: 12, strategy: "stratified" } });
496
+ expect(stratified.rows.map((entry) => entry.key)).toEqual(rows.map((entry) => entry.key));
497
+ expect(() => samplerows({ rows, policy: { rows: 0, strategy: "first" } })).toThrow(/positive whole number/);
498
+ expect(() => samplerows({ rows, policy: { rows: -1, strategy: "first" } })).toThrow(/positive whole number/);
499
+ expect(() => samplerows({ rows, policy: { rows: 2.5, strategy: "first" } })).toThrow(/positive whole number/);
500
+ const copied = samplerows({ rows, policy: { rows: 2, strategy: "first" } });
501
+ copied.rows[0]!.values.a = "mutated";
502
+ expect(rows[0]!.values.a).toBe("0");
503
+ });
504
+ });
505
+
506
+ describe("torture: source stamps and grid projection", () => {
507
+ it("stamps rows and cells while a repeated stamp id never duplicates and a second stamp overwrites cell links", () => {
508
+ const rows = [row("r1", { a: "1", b: "2" })];
509
+ const stamped = sourcestamp({ rows, stampid: "st1", url: "https://example.com", stepid: "s1", now });
510
+ expect(stamped.rows[0]?.cellstamps).toEqual({ a: "st1", b: "st1" });
511
+ const restamped = sourcestamp({
512
+ rows: stamped.rows,
513
+ stampid: "st1",
514
+ url: "https://example.com",
515
+ stepid: "s1",
516
+ now: now + 5,
517
+ });
518
+ expect(restamped.rows[0]?.stamps).toHaveLength(1);
519
+ expect(restamped.rows[0]?.stamps[0]?.capturedat).toBe(now);
520
+ const second = sourcestamp({
521
+ rows: stamped.rows,
522
+ stampid: "st2",
523
+ url: "https://example.com/2",
524
+ stepid: "s2",
525
+ now: now + 9,
526
+ });
527
+ expect(second.rows[0]?.stamps.map((stamp) => stamp.id)).toEqual(["st1", "st2"]);
528
+ expect(second.rows[0]?.cellstamps?.a).toBe("st2");
529
+ expect(() => sourcestamp({ rows, stampid: " ", url: "u", stepid: "s", now })).toThrow(/needs its id/);
530
+ expect(() => sourcestamp({ rows, stampid: "s", url: " ", stepid: "s", now })).toThrow(/names its url/);
531
+ expect(() => sourcestamp({ rows, stampid: "s", url: "u", stepid: " ", now })).toThrow(/names its step/);
532
+ });
533
+
534
+ it("projects the grid with column kinds, sorting, filtering, marks and refusals for unknown sort columns", () => {
535
+ const rows = [
536
+ row("r1", { name: "Devthink", price: "1299.50", seen: "2026-03-03", live: "true" }),
537
+ row("r2", { name: "Other", price: "42", seen: "2026-03-04", live: "false" }),
538
+ row("r3", { name: "Mixed", price: "n/a", seen: "2026-03-05", live: "true" }),
539
+ ];
540
+ const grid = pipelinegridpreview({ rows });
541
+ const kinds = Object.fromEntries(grid.columns.map((column) => [column.name, column.kind]));
542
+ expect(kinds.name).toBe("text");
543
+ expect(kinds.price).toBe("text");
544
+ expect(kinds.seen).toBe("date");
545
+ expect(kinds.live).toBe("boolean");
546
+ expect(grid.total).toBe(3);
547
+ expect(pipelinegridpreview({ rows: [] }).columns).toEqual([]);
548
+ expect(pipelinegridpreview({ rows: [], filter: "anything" }).rows).toEqual([]);
549
+ expect(pipelinegridpreview({ rows, filter: "DEVTHINK" }).rows.map((entry) => entry.key)).toEqual(["r1"]);
550
+ expect(pipelinegridpreview({ rows, filter: " " }).rows).toHaveLength(3);
551
+ expect(pipelinegridpreview({ rows, dedupekeys: ["r1", "r2"] }).rows.map((entry) => entry.deduplicated)).toEqual([
552
+ true,
553
+ true,
554
+ false,
555
+ ]);
556
+ expect(() => pipelinegridpreview({ rows, sort: { column: "ghost", direction: "ascending" } })).toThrow(
557
+ /knows no ghost/,
558
+ );
559
+ expect(
560
+ pipelinegridpreview({ rows, sort: { column: "name", direction: "descending" } }).rows.map((entry) => entry.key),
561
+ ).toEqual(["r2", "r3", "r1"]);
562
+ });
563
+ });
564
+
565
+ describe("torture: provlog redaction, append only and queries", () => {
566
+ it("redacts every secret shape from summaries before an entry lands", () => {
567
+ const entry = provlog({
568
+ entries: [],
569
+ id: "l1",
570
+ pipelineid: "p1",
571
+ runid: "run1",
572
+ operation: "stream",
573
+ rowkeys: ["r1"],
574
+ summary:
575
+ "streamed with token=abc123 and api-key: xyz and sk-abcdefghijklmnopqrstuv and ghp_AAAA0123456789aaaaaa and password:hunter2",
576
+ now,
577
+ }).entry;
578
+ expect(entry.summary).toContain("token=[redacted]");
579
+ expect(entry.summary).toContain("api-key: [redacted]");
580
+ expect(entry.summary).toContain("[redacted key]");
581
+ expect(entry.summary).toContain("[redacted token]");
582
+ expect(entry.summary).toContain("password:[redacted]");
583
+ expect(entry.summary).not.toContain("hunter2");
584
+ expect(entry.summary).not.toContain("abc123");
585
+ expect(entry.summary).not.toContain("ghp_");
586
+ });
587
+
588
+ it("redacts pem private key blocks inside a summary", () => {
589
+ const pem = `-----BEGIN RSA PRIVATE KEY-----${"A".repeat(64)}-----END RSA PRIVATE KEY-----`;
590
+ const entry = provlog({
591
+ entries: [],
592
+ id: "l2",
593
+ pipelineid: "p1",
594
+ runid: "run1",
595
+ operation: "transform",
596
+ rowkeys: [],
597
+ summary: `the pass carried ${pem} through`,
598
+ now,
599
+ }).entry;
600
+ expect(entry.summary).toContain("[redacted private key]");
601
+ expect(entry.summary).not.toContain("BEGIN RSA");
602
+ });
603
+
604
+ it("refuses duplicate ids, empty ids and empty summaries while the query orders by time then id", () => {
605
+ const one = provlog({
606
+ entries: [],
607
+ id: "l1",
608
+ pipelineid: "p1",
609
+ runid: "run1",
610
+ operation: "stream",
611
+ rowkeys: ["r1"],
612
+ summary: "one",
613
+ now,
614
+ });
615
+ const two = provlog({
616
+ entries: one.entries,
617
+ id: "l2",
618
+ pipelineid: "p1",
619
+ runid: "run1",
620
+ operation: "dedupe",
621
+ rowkeys: ["r1"],
622
+ summary: "two",
623
+ now,
624
+ });
625
+ expect(() =>
626
+ provlog({
627
+ entries: two.entries,
628
+ id: "l1",
629
+ pipelineid: "p1",
630
+ runid: "run1",
631
+ operation: "stream",
632
+ rowkeys: [],
633
+ summary: "again",
634
+ now,
635
+ }),
636
+ ).toThrow(/append only/);
637
+ expect(() =>
638
+ provlog({
639
+ entries: [],
640
+ id: " ",
641
+ pipelineid: "p1",
642
+ runid: "run1",
643
+ operation: "stream",
644
+ rowkeys: [],
645
+ summary: "s",
646
+ now,
647
+ }),
648
+ ).toThrow(/needs its id/);
649
+ expect(() =>
650
+ provlog({
651
+ entries: [],
652
+ id: "l3",
653
+ pipelineid: " ",
654
+ runid: "run1",
655
+ operation: "stream",
656
+ rowkeys: [],
657
+ summary: "s",
658
+ now,
659
+ }),
660
+ ).toThrow(/names its pipeline/);
661
+ expect(() =>
662
+ provlog({
663
+ entries: [],
664
+ id: "l4",
665
+ pipelineid: "p1",
666
+ runid: "run1",
667
+ operation: "stream",
668
+ rowkeys: [],
669
+ summary: " ",
670
+ now,
671
+ }),
672
+ ).toThrow(/needs its summary/);
673
+ const tie = provlog({
674
+ entries: two.entries,
675
+ id: "l0",
676
+ pipelineid: "p1",
677
+ runid: "run1",
678
+ operation: "sample",
679
+ rowkeys: ["r1"],
680
+ summary: "tie",
681
+ now,
682
+ });
683
+ expect(provlogquery(tie.entries, "r1").map((entry) => entry.id)).toEqual(["l0", "l1", "l2"]);
684
+ expect(provlogquery(tie.entries, "ghost")).toEqual([]);
685
+ });
686
+
687
+ it("counts the pipeline states per run with empty inputs", () => {
688
+ expect(pipelinestate({ pipelines: [] })).toEqual({ running: 0, paused: 0, finished: 0, perrun: [] });
689
+ const state = pipelinestate({
690
+ pipelines: [
691
+ pipeline(),
692
+ pipeline({ id: "p2", state: "paused", runid: "run2" }),
693
+ pipeline({ id: "p3", state: "finished" }),
694
+ ],
695
+ });
696
+ expect(state).toEqual({
697
+ running: 1,
698
+ paused: 1,
699
+ finished: 1,
700
+ perrun: [
701
+ { runid: "run1", running: 1, paused: 0, finished: 1 },
702
+ { runid: "run2", running: 0, paused: 1, finished: 0 },
703
+ ],
704
+ });
705
+ const batch = extractbatchof({
706
+ id: "b1",
707
+ pipeline: pipeline(),
708
+ rows: [row("r1", { a: "1" })],
709
+ cursor: { pipelineid: "p1", offset: 1, chunk: 1, updatedat: now },
710
+ now,
711
+ });
712
+ expect(batch).toMatchObject({ id: "b1", pipelineid: "p1", at: now });
713
+ expect(() =>
714
+ extractbatchof({
715
+ id: " ",
716
+ pipeline: pipeline(),
717
+ rows: [],
718
+ cursor: { pipelineid: "p1", offset: 0, chunk: 0, updatedat: now },
719
+ now,
720
+ }),
721
+ ).toThrow(/needs its id/);
722
+ });
723
+ });
724
+
725
+ describe("torture: streamparse windows, chunks and validation", () => {
726
+ it("splits byte ranges with zero, fractional and huge bounds while refusing non positive chunk bytes", () => {
727
+ expect(streamparsewindows({ bytes: 0, chunkbytes: 5 })).toEqual({ chunks: 0, windows: [] });
728
+ expect(streamparsewindows({ bytes: 10, chunkbytes: 4 }).windows).toEqual([
729
+ { index: 0, start: 0, end: 4 },
730
+ { index: 1, start: 4, end: 8 },
731
+ { index: 2, start: 8, end: 10 },
732
+ ]);
733
+ expect(streamparsewindows({ bytes: 10, chunkbytes: 2.5 }).chunks).toBe(4);
734
+ expect(streamparsewindows({ bytes: 10, chunkbytes: 1_000_000 })).toEqual({
735
+ chunks: 1,
736
+ windows: [{ index: 0, start: 0, end: 10 }],
737
+ });
738
+ expect(() => streamparsewindows({ bytes: 10, chunkbytes: 0 })).toThrow(/positive number of bytes/);
739
+ expect(() => streamparsewindows({ bytes: 10, chunkbytes: -1 })).toThrow(/positive number of bytes/);
740
+ });
741
+
742
+ it("tokenizes chunks with element, attribute and text tokens while refusing over bound chunks", () => {
743
+ const chunk = streamparsechunkof({
744
+ index: 0,
745
+ text: 'hello <div class="a">world</div> tail',
746
+ chunkbytes: 100,
747
+ last: true,
748
+ });
749
+ expect(chunk.tokens.map((token) => token.kind)).toEqual([
750
+ "text",
751
+ "element",
752
+ "attribute",
753
+ "text",
754
+ "element",
755
+ "text",
756
+ ]);
757
+ expect(chunk.tokens.find((token) => token.kind === "attribute")?.text).toBe('class="a"');
758
+ expect(chunk.observations).toEqual(["hello", "world", "tail"]);
759
+ expect(chunk.bytes).toBe(37);
760
+ expect(chunk.complete).toBe(true);
761
+ expect(streamparsechunkof({ index: 3, text: "", chunkbytes: 10, last: false }).tokens).toEqual([]);
762
+ expect(() => streamparsechunkof({ index: 0, text: "a".repeat(11), chunkbytes: 10, last: true })).toThrow(
763
+ /over the 10 byte bound/,
764
+ );
765
+ });
766
+
767
+ it("validates chunk carriers through schemastrict refusals", () => {
768
+ const chunk = streamparsechunkof({ index: 2, text: "x", chunkbytes: 10, last: false });
769
+ expect(validatesetreamparsechunk(chunk, 2).valid).toBe(true);
770
+ expect(validatesetreamparsechunk(null, 0).reason).toMatch(/plain object/);
771
+ expect(validatesetreamparsechunk([chunk], 2).reason).toMatch(/plain object/);
772
+ expect(validatesetreamparsechunk({ ...chunk, index: 3 }, 2).reason).toMatch(/index 2/);
773
+ expect(validatesetreamparsechunk({ ...chunk, tokens: "no" }, 2).reason).toMatch(/tokens/);
774
+ expect(validatesetreamparsechunk({ ...chunk, bytes: "no" }, 2).reason).toMatch(/byte count/);
775
+ expect(validatesetreamparsechunk({ ...chunk, complete: "no" }, 2).reason).toMatch(/completion flag/);
776
+ });
777
+
778
+ it("yields progressive observations and memory bounds", () => {
779
+ expect(streamparseobservations([])).toEqual({ progressive: [], total: 0 });
780
+ const chunks = [
781
+ streamparsechunkof({ index: 0, text: "one", chunkbytes: 10, last: false }),
782
+ streamparsechunkof({ index: 1, text: "two <b>three</b>", chunkbytes: 20, last: true }),
783
+ ];
784
+ const yielded = streamparseobservations(chunks);
785
+ expect(yielded.progressive).toEqual([["one"], ["two", "three"]]);
786
+ expect(yielded.total).toBe(3);
787
+ expect(streamparsememorybound({ chunkbytes: 64, chunks: 0 })).toEqual({ peak: 64, fullpage: 0, saved: 0 });
788
+ expect(streamparsememorybound({ chunkbytes: 64, chunks: 100 })).toEqual({ peak: 64, fullpage: 6400, saved: 6336 });
789
+ });
790
+
791
+ it("shapes worker tasks with positive bounds", () => {
792
+ expect(streamparsetaskof({ runid: "r1", stepid: "s1", chunkbytes: 64, chunks: 3 })).toEqual({
793
+ task: "streamparse",
794
+ runid: "r1",
795
+ stepid: "s1",
796
+ chunkbytes: 64,
797
+ chunks: 3,
798
+ });
799
+ expect(() => streamparsetaskof({ runid: "r1", stepid: "s1", chunkbytes: 0, chunks: 3 })).toThrow(
800
+ /positive chunk bound/,
801
+ );
802
+ expect(() => streamparsetaskof({ runid: "r1", stepid: "s1", chunkbytes: -1, chunks: 3 })).toThrow(
803
+ /positive chunk bound/,
804
+ );
805
+ expect(
806
+ chunkextracttaskof({ runid: "r1", stepid: "s1", tableid: "t1", fingerprint: "f1", rowindex: 4, window: 2 }),
807
+ ).toMatchObject({ task: "chunkextract", tableid: "t1" });
808
+ expect(() =>
809
+ chunkextracttaskof({ runid: "r1", stepid: "s1", tableid: "t1", fingerprint: "f1", rowindex: 4, window: 0 }),
810
+ ).toThrow(/positive row window/);
811
+ expect(() =>
812
+ chunkextracttaskof({ runid: "r1", stepid: "s1", tableid: "t1", fingerprint: "f1", rowindex: 4, window: -3 }),
813
+ ).toThrow(/positive row window/);
814
+ });
815
+ });
816
+
817
+ describe("torture: chunkextract cursors, windows and merges", () => {
818
+ it("opens cursors with zero row tables, defaults and refusals", () => {
819
+ expect(openchunkextractcursor({ tableid: "t1", fingerprint: "f1", rows: 0 })).toMatchObject({
820
+ rowindex: 0,
821
+ complete: true,
822
+ });
823
+ expect(openchunkextractcursor({ tableid: "t1", fingerprint: "f1", rows: 10 })).toMatchObject({
824
+ window: 10,
825
+ complete: false,
826
+ });
827
+ expect(openchunkextractcursor({ tableid: "t1", fingerprint: "f1", rows: 10, window: 3 })).toMatchObject({
828
+ window: 3,
829
+ complete: false,
830
+ });
831
+ expect(() => openchunkextractcursor({ tableid: " ", fingerprint: "f", rows: 1 })).toThrow(/table id/);
832
+ expect(() => openchunkextractcursor({ tableid: "t", fingerprint: " ", rows: 1 })).toThrow(/fingerprint/);
833
+ expect(() => openchunkextractcursor({ tableid: "t", fingerprint: "f", rows: 1, window: 0 })).toThrow(
834
+ /positive row count/,
835
+ );
836
+ expect(() => openchunkextractcursor({ tableid: "t", fingerprint: "f", rows: 1, window: -2 })).toThrow(
837
+ /positive row count/,
838
+ );
839
+ });
840
+
841
+ it("slices windows at the end, past the end and larger than the table", () => {
842
+ const rows = [{ a: "1" }, { a: "2" }, { a: "3" }, { a: "4" }, { a: "5" }];
843
+ const first = chunkextractwindow({
844
+ cursor: openchunkextractcursor({ tableid: "t1", fingerprint: "f1", rows: 5, window: 2 }),
845
+ rows,
846
+ });
847
+ expect(first.rows).toEqual([{ a: "1" }, { a: "2" }]);
848
+ expect(first.complete).toBe(false);
849
+ const oversized = chunkextractwindow({
850
+ cursor: { tableid: "t1", fingerprint: "f1", rowindex: 0, window: 100, complete: false },
851
+ rows,
852
+ });
853
+ expect(oversized.rows).toHaveLength(5);
854
+ expect(oversized.complete).toBe(true);
855
+ const atend = chunkextractwindow({
856
+ cursor: { tableid: "t1", fingerprint: "f1", rowindex: 5, window: 2, complete: true },
857
+ rows,
858
+ });
859
+ expect(atend.rows).toEqual([]);
860
+ expect(atend.complete).toBe(true);
861
+ expect(() =>
862
+ chunkextractwindow({
863
+ cursor: { tableid: "t1", fingerprint: "f1", rowindex: 6, window: 2, complete: false },
864
+ rows,
865
+ }),
866
+ ).toThrow(/past the end/);
867
+ expect(
868
+ chunkextractwindow({ cursor: openchunkextractcursor({ tableid: "t", fingerprint: "f", rows: 0 }), rows: [] }),
869
+ ).toMatchObject({ rows: [], complete: true });
870
+ });
871
+
872
+ it("resumes only the same table with the same fingerprint while a completed cursor never resumes", () => {
873
+ const cursor = openchunkextractcursor({ tableid: "t1", fingerprint: "f1", rows: 10, window: 2 });
874
+ expect(resumechunkextract({ cursor, tableid: "t1", fingerprint: "f1" })).toMatchObject({ resumed: true });
875
+ expect(resumechunkextract({ cursor, tableid: "t2", fingerprint: "f1" }).reason).toMatch(/belongs to the table t1/);
876
+ expect(resumechunkextract({ cursor, tableid: "t1", fingerprint: "f2" }).reason).toMatch(/fingerprint changed/);
877
+ expect(
878
+ resumechunkextract({ cursor: { ...cursor, complete: true }, tableid: "t1", fingerprint: "f1" }).reason,
879
+ ).toMatch(/already completed/);
880
+ });
881
+
882
+ it("merges windows without duplicating rows and reports the duplicates", () => {
883
+ const rows = [{ a: "1" }, { a: "2" }];
884
+ const windows = [
885
+ { cursor: { tableid: "t", fingerprint: "f", rowindex: 2, window: 2, complete: true }, rows, complete: true },
886
+ { cursor: { tableid: "t", fingerprint: "f", rowindex: 2, window: 2, complete: true }, rows, complete: true },
887
+ {
888
+ cursor: { tableid: "t", fingerprint: "f", rowindex: 4, window: 2, complete: true },
889
+ rows: [{ a: "3" }],
890
+ complete: true,
891
+ },
892
+ ];
893
+ const merged = mergechunkwindows(windows);
894
+ expect(merged.rows).toEqual([{ a: "1" }, { a: "2" }, { a: "3" }]);
895
+ expect(merged.duplicates).toBe(2);
896
+ expect(mergechunkwindows([])).toEqual({ rows: [], duplicates: 0 });
897
+ const reordered = mergechunkwindows([
898
+ {
899
+ cursor: { tableid: "t", fingerprint: "f", rowindex: 1, window: 1, complete: true },
900
+ rows: [{ b: "2", a: "1" }],
901
+ complete: true,
902
+ },
903
+ {
904
+ cursor: { tableid: "t", fingerprint: "f", rowindex: 0, window: 1, complete: true },
905
+ rows: [{ a: "1", b: "2" }],
906
+ complete: true,
907
+ },
908
+ ]);
909
+ expect(reordered.duplicates).toBe(1);
910
+ });
911
+
912
+ it("reads progress with zero totals and cursors past the end", () => {
913
+ expect(
914
+ chunkextractprogress({ tableid: "t", fingerprint: "f", rowindex: 3, window: 2, complete: false }, 5),
915
+ ).toEqual({ extracted: 3, total: 5, share: 0.6, complete: false });
916
+ expect(
917
+ chunkextractprogress({ tableid: "t", fingerprint: "f", rowindex: 0, window: 2, complete: false }, 0).share,
918
+ ).toBe(1);
919
+ expect(
920
+ chunkextractprogress({ tableid: "t", fingerprint: "f", rowindex: 9, window: 2, complete: false }, 5).share,
921
+ ).toBe(1.8);
922
+ });
923
+ });