@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.
- package/CHANGELOG.md +27 -0
- package/accounts.ts +2087 -0
- package/agent.ts +2331 -0
- package/antigravity.ts +2139 -0
- package/apifreeze.ts +4146 -0
- package/auth.ts +642 -0
- package/background.ts +16837 -0
- package/bridge.ts +2080 -0
- package/build.ts +54 -0
- package/bun.ts +26 -0
- package/capture.ts +1614 -0
- package/checksecrets.ts +72 -0
- package/cjs.ts +9 -0
- package/cli.ts +3500 -0
- package/commands.ts +1101 -0
- package/companion.ts +450 -0
- package/compatibility.ts +37 -0
- package/config.ts +3137 -0
- package/constants.ts +943 -0
- package/core.ts +1473 -0
- package/crossbrowser.ts +1359 -0
- package/dashdone.ts +605 -0
- package/data.ts +943 -0
- package/database.ts +133 -0
- package/debug.ts +850 -0
- package/deno.ts +27 -0
- package/devthink.ts +707 -0
- package/dist/accounts.d.ts +409 -0
- package/dist/agent.d.ts +837 -0
- package/dist/antigravity.d.ts +380 -0
- package/dist/apifreeze.d.ts +113 -0
- package/dist/auth.d.ts +224 -0
- package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
- package/dist/capture.d.ts +595 -0
- package/dist/cjs.d.ts +6 -0
- package/{cli.js → dist/cli.js} +2 -2
- package/dist/commands.d.ts +310 -0
- package/dist/compatibility.d.ts +17 -0
- package/dist/config.d.ts +1853 -0
- package/dist/constants.d.ts +637 -0
- package/dist/core.d.ts +361 -0
- package/dist/data.d.ts +330 -0
- package/dist/database.d.ts +24 -0
- package/dist/debug.d.ts +270 -0
- package/dist/engine.d.ts +74 -0
- package/dist/environments.d.ts +244 -0
- package/dist/evidence.d.ts +277 -0
- package/dist/export.d.ts +311 -0
- package/dist/fingerprint.d.ts +594 -0
- package/dist/flow.d.ts +300 -0
- package/dist/gates.d.ts +103 -0
- package/dist/identity.d.ts +51 -0
- package/dist/ids.d.ts +7 -0
- package/dist/llm.d.ts +652 -0
- package/dist/models.d.ts +1807 -0
- package/dist/net.d.ts +325 -0
- package/dist/neutral.d.ts +41 -0
- package/dist/oauth.d.ts +703 -0
- package/dist/page.d.ts +1166 -0
- package/dist/perf.d.ts +481 -0
- package/dist/plan.d.ts +134 -0
- package/dist/project.d.ts +395 -0
- package/dist/providers.d.ts +37 -0
- package/dist/quota.d.ts +453 -0
- package/dist/recovery.d.ts +621 -0
- package/dist/request.d.ts +1492 -0
- package/dist/run.d.ts +779 -0
- package/dist/runtime.d.ts +212 -0
- package/dist/search.d.ts +375 -0
- package/dist/security.d.ts +534 -0
- package/dist/serve.d.ts +136 -0
- package/dist/session.d.ts +296 -0
- package/dist/storage.d.ts +53 -0
- package/dist/streaming.d.ts +1119 -0
- package/dist/swarm.d.ts +328 -0
- package/dist/tools.d.ts +170 -0
- package/dist/types.d.ts +8357 -0
- package/dist/utils.d.ts +82 -0
- package/dist/version.d.ts +7 -0
- package/dist/versionregistry.d.ts +406 -0
- package/dist/views.d.ts +579 -0
- package/dist/workbenchsession.d.ts +69 -0
- package/dist/workflow.d.ts +649 -0
- package/docs/readme.md +92 -0
- package/docs/schemas/antigravity.json +192 -0
- package/engine.ts +1524 -0
- package/environments.ts +1005 -0
- package/evidence.ts +643 -0
- package/export.ts +889 -0
- package/fingerprint.ts +1458 -0
- package/flow.ts +1190 -0
- package/gates.ts +307 -0
- package/gateway.ts +34 -0
- package/hardening.ts +191 -0
- package/headless.ts +369 -0
- package/http.ts +2110 -0
- package/identity.ts +246 -0
- package/ids.ts +26 -0
- package/index.ts +369 -0
- package/llm.ts +2682 -0
- package/mcp.ts +1835 -0
- package/{memory.js → memory.ts} +3834 -2310
- package/models.ts +3805 -0
- package/modes.ts +43 -0
- package/net.ts +1214 -0
- package/neutral.ts +362 -0
- package/node.ts +26 -0
- package/oauth.ts +2216 -0
- package/offscreen.ts +170 -0
- package/pack.ts +956 -0
- package/package.json +175 -82
- package/page.ts +7824 -0
- package/pagebridge.ts +1116 -0
- package/perf.ts +957 -0
- package/plan.ts +672 -0
- package/plugin.ts +76 -0
- package/{policy.js → policy.ts} +5073 -4679
- package/prisma.config.ts +27 -0
- package/progress.ts +1308 -0
- package/project.ts +1291 -0
- package/protocol.ts +4187 -0
- package/providers.ts +246 -0
- package/quota.ts +1887 -0
- package/recovery.ts +2433 -0
- package/request.ts +4482 -0
- package/run.ts +2135 -0
- package/runtime.ts +526 -0
- package/search.ts +1760 -0
- package/security.ts +1673 -0
- package/serve.ts +419 -0
- package/server.ts +5538 -0
- package/session.ts +1162 -0
- package/storage.ts +182 -0
- package/streaming.ts +4101 -0
- package/swarm.ts +884 -0
- package/sync.ts +56 -0
- package/tests/agentcert.test.ts +128 -0
- package/tests/agentfleet.test.ts +214 -0
- package/tests/agentreview.test.ts +241 -0
- package/tests/agentstream.test.ts +335 -0
- package/tests/agentwork.test.ts +356 -0
- package/tests/apifreeze.test.ts +290 -0
- package/tests/apimap.test.ts +112 -0
- package/tests/approvalgate.test.ts +175 -0
- package/tests/artifactmanifest.test.ts +204 -0
- package/tests/attentionfeed.test.ts +163 -0
- package/tests/backgroundruns.test.ts +100 -0
- package/tests/batchscheduling.test.ts +131 -0
- package/tests/blackboard.test.ts +238 -0
- package/tests/bridge.test.ts +1088 -0
- package/tests/browsercoverage.test.ts +134 -0
- package/tests/browserpolyfills.test.ts +124 -0
- package/tests/build.mjs +1260 -0
- package/tests/clientauth.test.ts +223 -0
- package/tests/clitools.test.ts +578 -0
- package/tests/confirmgates.test.ts +190 -0
- package/tests/containerpack.test.ts +143 -0
- package/tests/controlflow.test.ts +2120 -0
- package/tests/coordination.test.ts +345 -0
- package/tests/copyscreen.test.ts +139 -0
- package/tests/core.test.ts +417 -0
- package/tests/costcert.test.ts +95 -0
- package/tests/crossbrowser.test.ts +115 -0
- package/tests/cspaudit.test.ts +79 -0
- package/tests/dashdone.test.ts +615 -0
- package/tests/datagrid.test.ts +137 -0
- package/tests/doccheck.test.ts +107 -0
- package/tests/dombatch.test.ts +109 -0
- package/tests/emulation.test.ts +282 -0
- package/tests/environments.test.ts +259 -0
- package/tests/evidenceviews.test.ts +88 -0
- package/tests/exporttools.test.ts +185 -0
- package/tests/firefoxprep.test.ts +134 -0
- package/tests/flowlibrary.test.ts +242 -0
- package/tests/flowrun.test.ts +303 -0
- package/tests/forensics.test.ts +439 -0
- package/tests/gateway.test.ts +939 -0
- package/tests/headlesslib.test.ts +171 -0
- package/tests/httpstream.test.ts +277 -0
- package/tests/immutablelog.test.ts +157 -0
- package/tests/inboundguard.test.ts +177 -0
- package/tests/lazyload.test.ts +72 -0
- package/tests/librarymodes.test.ts +623 -0
- package/tests/llm.test.ts +696 -0
- package/tests/manifest.test.ts +226 -0
- package/tests/maskinputs.test.ts +150 -0
- package/tests/matrixverify.test.ts +111 -0
- package/tests/mavenpack.test.ts +75 -0
- package/tests/mcpserver.test.ts +704 -0
- package/tests/memory.test.ts +4155 -0
- package/tests/memorycare.test.ts +230 -0
- package/tests/migrateplan.test.ts +349 -0
- package/tests/minimization.test.ts +414 -0
- package/tests/modelroute.test.ts +112 -0
- package/tests/multiagent.test.ts +440 -0
- package/tests/nativebridge.test.ts +807 -0
- package/tests/navigation.test.ts +284 -0
- package/tests/nugetpack.test.ts +83 -0
- package/tests/orchestration.test.ts +449 -0
- package/tests/originpolicy.test.ts +405 -0
- package/tests/outputcompare.test.ts +97 -0
- package/tests/package.test.ts +91 -0
- package/tests/pagebridge.test.ts +5909 -0
- package/tests/pentest.test.ts +75 -0
- package/tests/perfrecords.test.ts +257 -0
- package/tests/permdiff.test.ts +85 -0
- package/tests/phishguard.test.ts +128 -0
- package/tests/pickerviews.test.ts +155 -0
- package/tests/pipelines.test.ts +381 -0
- package/tests/planlint.test.ts +240 -0
- package/tests/planreview.test.ts +383 -0
- package/tests/platformtargets.test.ts +56 -0
- package/tests/policy.test.ts +8284 -0
- package/tests/portability.test.ts +96 -0
- package/tests/progress.test.ts +1036 -0
- package/tests/promptlibrary.test.ts +116 -0
- package/tests/protocol.test.ts +4793 -0
- package/tests/publishingpipeline.test.ts +314 -0
- package/tests/quickactions.test.ts +164 -0
- package/tests/readiness.test.ts +78 -0
- package/tests/recipes.test.ts +163 -0
- package/tests/redactshots.test.ts +216 -0
- package/tests/registernvidiakeys.ts +51 -0
- package/tests/relayserve.test.ts +304 -0
- package/tests/releasenotes.test.ts +276 -0
- package/tests/resilience.test.ts +467 -0
- package/tests/resourceaware.test.ts +187 -0
- package/tests/runbudget.test.ts +125 -0
- package/tests/runhistory.test.ts +299 -0
- package/tests/runreplay.test.ts +113 -0
- package/tests/runresume.test.ts +184 -0
- package/tests/runstate.test.ts +288 -0
- package/tests/runtimeadapters.test.ts +251 -0
- package/tests/safariskeleton.test.ts +115 -0
- package/tests/sandboxframe.test.ts +196 -0
- package/tests/sbom.test.ts +105 -0
- package/tests/secretvault.test.ts +188 -0
- package/tests/selcache.test.ts +111 -0
- package/tests/sessioninterface.test.ts +791 -0
- package/tests/sessions.test.ts +591 -0
- package/tests/siteprefs.test.ts +228 -0
- package/tests/snapshotdelta.test.ts +165 -0
- package/tests/statusviews.test.ts +237 -0
- package/tests/stepmeter.test.ts +200 -0
- package/tests/streamparse.test.ts +163 -0
- package/tests/stylepruning.test.ts +90 -0
- package/tests/surfaces.test.ts +282 -0
- package/tests/sweep.test.ts +78 -0
- package/tests/syncbridge.test.ts +164 -0
- package/tests/taskqueue.test.ts +206 -0
- package/tests/toolcalls.test.ts +336 -0
- package/tests/toolcatalog.test.ts +201 -0
- package/tests/torture-agent.test.ts +706 -0
- package/tests/torture-auth.test.ts +461 -0
- package/tests/torture-commands.test.ts +895 -0
- package/tests/torture-data.test.ts +923 -0
- package/tests/torture-environments.test.ts +285 -0
- package/tests/torture-export.test.ts +263 -0
- package/tests/torture-http.test.ts +1379 -0
- package/tests/torture-llm.test.ts +808 -0
- package/tests/torture-memory.test.ts +1084 -0
- package/tests/torture-plan.test.ts +829 -0
- package/tests/torture-run.test.ts +626 -0
- package/tests/torture-session.test.ts +928 -0
- package/tests/torture-swarm.test.ts +1245 -0
- package/tests/torture-tools.test.ts +964 -0
- package/tests/torture-workflow.test.ts +2354 -0
- package/tests/tourviews.test.ts +52 -0
- package/tests/transparency.test.ts +147 -0
- package/tests/trigger.test.ts +941 -0
- package/tests/v1sunset.test.ts +428 -0
- package/tests/virtlist.test.ts +94 -0
- package/tests/vision.test.ts +540 -0
- package/tests/vsixpack.test.ts +145 -0
- package/tests/webapi.test.ts +523 -0
- package/tests/workflow.test.ts +1785 -0
- package/tests/workfloweditor.test.ts +759 -0
- package/tests/xpipack.test.ts +128 -0
- package/tools.ts +734 -0
- package/types.ts +9908 -0
- package/umd.ts +9 -0
- package/utils.ts +249 -0
- package/version.ts +8 -0
- package/versionregistry.ts +1310 -0
- package/views.ts +2153 -0
- package/vitest.config.ts +12 -0
- package/web/App.tsx +78 -0
- package/web/README.md +47 -0
- package/web/capacitor.config.ts +46 -0
- package/web/const.ts +19 -0
- package/web/control.shell.tsx +66 -0
- package/web/dashboardpage.ts +554 -0
- package/web/db.ts +493 -0
- package/web/design.html +541 -0
- package/web/gatewayview/Gateway.tsx +345 -0
- package/web/gatewayview/config.ts +1042 -0
- package/web/gatewayview/definition.ts +174 -0
- package/web/icons.ts +15 -0
- package/{manifest.json → web/manifest.json} +1 -1
- package/web/optionspage.ts +691 -0
- package/web/popup.ts +1651 -0
- package/web/schema.prisma +176 -0
- package/web/sidepanel.ts +7941 -0
- package/web/sitemanifest.json +13 -0
- package/web/terminal-ui.tsx +622 -0
- package/web/transparencypage.ts +541 -0
- package/web/vite.config.ts +39 -0
- package/web/workspace.ts +23 -0
- package/workbenchmemory.ts +104 -0
- package/workbenchsession.ts +304 -0
- package/workflow.ts +4495 -0
- package/bridge.d.ts.map +0 -1
- package/bridge.js +0 -1877
- package/bun.d.ts.map +0 -1
- package/bun.js +0 -52329
- package/caps/background.json +0 -712
- package/caps/cli.json +0 -360
- package/caps/library.json +0 -3611
- package/caps/mcp.json +0 -126
- package/caps/pagebridge.json +0 -377
- package/caps/popup.json +0 -51
- package/caps/sidepanel.json +0 -522
- package/checksums.txt +0 -142
- package/cli.d.ts.map +0 -1
- package/companion.d.ts.map +0 -1
- package/companion.js +0 -389
- package/crossbrowser.d.ts.map +0 -1
- package/crossbrowser.js +0 -1191
- package/dashdone.d.ts.map +0 -1
- package/dashdone.js +0 -368
- package/deno.d.ts.map +0 -1
- package/deno.js +0 -52325
- package/devthink.umd.js +0 -52346
- package/fixtures/bun.ts +0 -7
- package/fixtures/cjs.js +0 -6
- package/fixtures/deno.ts +0 -6
- package/fixtures/example-org-pagestate.json +0 -22
- package/fixtures/importers/automa-workflow.json +0 -18
- package/fixtures/importers/selenium-side.json +0 -20
- package/fixtures/importers/tabular-plan.csv +0 -7
- package/fixtures/importers/uivision-macro.json +0 -10
- package/fixtures/importers/v1-plan.json +0 -13
- package/fixtures/mcp-client.mjs +0 -14
- package/fixtures/pages/checkout.html +0 -192
- package/fixtures/pages/dashboard.html +0 -110
- package/fixtures/pages/feed.html +0 -113
- package/fixtures/pages/product-grid.html +0 -150
- package/fixtures/plans/form-inventory-plan.json +0 -11
- package/fixtures/plans/release-notes-plan.json +0 -11
- package/fixtures/recipes/agentscompete.json +0 -11
- package/fixtures/recipes/agentsforms.json +0 -11
- package/fixtures/recipes/agentsmonitor.json +0 -11
- package/fixtures/recipes/agentsreview.json +0 -29
- package/fixtures/recipes/agentswarm.json +0 -11
- package/fixtures/recipes/fillcalendar.json +0 -12
- package/fixtures/recipes/fillcheckout.json +0 -13
- package/fixtures/recipes/filldependentdropdowns.json +0 -12
- package/fixtures/recipes/fillfromcsv.json +0 -13
- package/fixtures/recipes/fillloginform.json +0 -12
- package/fixtures/recipes/fillmultistepwizard.json +0 -13
- package/fixtures/recipes/fillregistration.json +0 -12
- package/fixtures/recipes/fillsearchfilters.json +0 -12
- package/fixtures/recipes/filltypeahead.json +0 -11
- package/fixtures/recipes/monitoralerts.json +0 -12
- package/fixtures/recipes/monitoravailability.json +0 -11
- package/fixtures/recipes/monitornetworkquiet.json +0 -12
- package/fixtures/recipes/monitorpagechanges.json +0 -11
- package/fixtures/recipes/monitorpricechange.json +0 -11
- package/fixtures/recipes/monitorschedule.json +0 -12
- package/fixtures/recipes/scrapedynamicprices.json +0 -13
- package/fixtures/recipes/scrapeimagegallery.json +0 -12
- package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
- package/fixtures/recipes/scrapenestedlists.json +0 -12
- package/fixtures/recipes/scrapeproductgrid.json +0 -12
- package/fixtures/recipes/scrapereviews.json +0 -12
- package/fixtures/recipes/scrapesearchresults.json +0 -12
- package/fixtures/recipes/scrapetablepagination.json +0 -12
- package/fixtures/recipes/testaccessibility.json +0 -34
- package/fixtures/recipes/testconsoleerrors.json +0 -12
- package/fixtures/recipes/testdeeplinks.json +0 -12
- package/fixtures/recipes/testformvalidation.json +0 -13
- package/fixtures/recipes/testlayoutshift.json +0 -12
- package/fixtures/recipes/testlinkcheck.json +0 -12
- package/fixtures/recipes/testloadtiming.json +0 -12
- package/fixtures/recipes/testvisualdiff.json +0 -12
- package/fixtures/workflows/changelog-digest-workflow.json +0 -13
- package/fixtures/workflows/form-survey-workflow.json +0 -13
- package/gallery.json +0 -706
- package/hardening.d.ts.map +0 -1
- package/hardening.js +0 -188
- package/headless.d.ts.map +0 -1
- package/headless.js +0 -11522
- package/http.d.ts.map +0 -1
- package/http.js +0 -2165
- package/index.cjs +0 -54682
- package/index.d.ts.map +0 -1
- package/index.js +0 -52310
- package/index.neutral.js +0 -46343
- package/mcp.d.ts.map +0 -1
- package/mcp.js +0 -20515
- package/memory.d.ts.map +0 -1
- package/nativehost.template.json +0 -10
- package/node.cjs +0 -54686
- package/node.d.ts.map +0 -1
- package/pack.d.ts.map +0 -1
- package/pack.js +0 -805
- package/policy.d.ts.map +0 -1
- package/progress.d.ts.map +0 -1
- package/progress.js +0 -645
- package/protocol.d.ts.map +0 -1
- package/protocol.js +0 -8334
- package/schemas/audit.schema.json +0 -226
- package/schemas/capability.schema.json +0 -113
- package/schemas/envelope.schema.json +0 -162
- package/schemas/memory.schema.json +0 -51
- package/schemas/observation.schema.json +0 -113
- package/schemas/plan.schema.json +0 -208
- package/schemas/progress.schema.json +0 -126
- package/schemas/proposal.schema.json +0 -68
- package/schemas/session.schema.json +0 -82
- package/schemas/tool.schema.json +0 -94
- package/server.d.ts.map +0 -1
- package/server.js +0 -28877
- package/umd-example.html +0 -18
- package/umd.d.ts.map +0 -1
- /package/{bun.d.ts → dist/bun.d.ts} +0 -0
- /package/{cli.d.ts → dist/cli.d.ts} +0 -0
- /package/{companion.d.ts → dist/companion.d.ts} +0 -0
- /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
- /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
- /package/{deno.d.ts → dist/deno.d.ts} +0 -0
- /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
- /package/{headless.d.ts → dist/headless.d.ts} +0 -0
- /package/{http.d.ts → dist/http.d.ts} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
- /package/{memory.d.ts → dist/memory.d.ts} +0 -0
- /package/{node.d.ts → dist/node.d.ts} +0 -0
- /package/{pack.d.ts → dist/pack.d.ts} +0 -0
- /package/{policy.d.ts → dist/policy.d.ts} +0 -0
- /package/{progress.d.ts → dist/progress.d.ts} +0 -0
- /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
- /package/{server.d.ts → dist/server.d.ts} +0 -0
- /package/{umd.d.ts → dist/umd.d.ts} +0 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
applyoverride,
|
|
5
|
+
buildsteplibrary,
|
|
6
|
+
diffversions,
|
|
7
|
+
expandtemplate,
|
|
8
|
+
exportworkflow,
|
|
9
|
+
groupselect,
|
|
10
|
+
importworkflow,
|
|
11
|
+
loadworkflow,
|
|
12
|
+
markbreakpoint,
|
|
13
|
+
minimapfocus,
|
|
14
|
+
palettenodes,
|
|
15
|
+
reordersteps,
|
|
16
|
+
renderminimap,
|
|
17
|
+
runtobreakpoint,
|
|
18
|
+
saveworkflow,
|
|
19
|
+
searchsteps,
|
|
20
|
+
snapnode,
|
|
21
|
+
undoedit,
|
|
22
|
+
redoedit,
|
|
23
|
+
addedge,
|
|
24
|
+
removeedge,
|
|
25
|
+
removenode,
|
|
26
|
+
addnode,
|
|
27
|
+
editstep,
|
|
28
|
+
bindparam,
|
|
29
|
+
zoomcanvas,
|
|
30
|
+
} from "../workflow.js";
|
|
31
|
+
import {
|
|
32
|
+
actionrisk,
|
|
33
|
+
dryrunprojection,
|
|
34
|
+
editorsavegate,
|
|
35
|
+
exportcontentreview,
|
|
36
|
+
runreviewgranted,
|
|
37
|
+
validatesiteoverride,
|
|
38
|
+
watchdogconfigvalid,
|
|
39
|
+
} from "../policy.js";
|
|
40
|
+
import {
|
|
41
|
+
composeworkflow,
|
|
42
|
+
expandblocks,
|
|
43
|
+
newworkflowrun,
|
|
44
|
+
runworkflow,
|
|
45
|
+
steptemplateof,
|
|
46
|
+
workflowblockof,
|
|
47
|
+
workflowstepof,
|
|
48
|
+
watchdogpass,
|
|
49
|
+
} from "../workflow.js";
|
|
50
|
+
import { editorstate, runhistoryquery, runhistoryreport, workflowfileversion } from "../protocol.js";
|
|
51
|
+
import type { editormodel, editorlayout, steptemplate, toolstep, variablescope, workflowrecord } from "../types.js";
|
|
52
|
+
|
|
53
|
+
const now = 1_800_000_000_000;
|
|
54
|
+
|
|
55
|
+
function kindallowed(kind: string): boolean {
|
|
56
|
+
try {
|
|
57
|
+
actionrisk(kind as toolstep["kind"]);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function riskof(kind: string): "read" | "interaction" | "sensitive" {
|
|
65
|
+
return actionrisk(kind as toolstep["kind"]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Composes one small read workflow of a top step, one block invocation and one closing step with bindings. */
|
|
69
|
+
function composedworkflow(): workflowrecord {
|
|
70
|
+
const inner = workflowblockof({
|
|
71
|
+
name: "inner",
|
|
72
|
+
label: "Inner block",
|
|
73
|
+
steps: [
|
|
74
|
+
{ id: "i1", kind: "readtext", label: "Read the heading", target: "h1" },
|
|
75
|
+
{ id: "i2", kind: "wait", label: "Inner wait", value: "5" },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
if (!inner) throw new Error("The inner block did not normalize.");
|
|
79
|
+
return composeworkflow({
|
|
80
|
+
id: "wf1",
|
|
81
|
+
name: "roundtrip",
|
|
82
|
+
version: 3,
|
|
83
|
+
origins: ["https://example.com"],
|
|
84
|
+
steps: [
|
|
85
|
+
{ id: "s1", kind: "readtext", label: "Open read", target: "p" },
|
|
86
|
+
{ block: "inner", label: "Call inner" },
|
|
87
|
+
{
|
|
88
|
+
id: "s2",
|
|
89
|
+
kind: "wait",
|
|
90
|
+
label: "Close wait",
|
|
91
|
+
value: "10",
|
|
92
|
+
bindings: [{ variable: "heading", kind: "string", stepid: "i1" }],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
blocks: [inner],
|
|
96
|
+
now,
|
|
97
|
+
kindallowed,
|
|
98
|
+
riskof,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe("workflow editor palette and library", () => {
|
|
103
|
+
it("lists the curated palette across the five categories", () => {
|
|
104
|
+
const categories = [...new Set(palettenodes.map((node) => node.category))];
|
|
105
|
+
expect(categories.sort()).toEqual(["actions", "controlflow", "triggers", "variables", "waits"]);
|
|
106
|
+
expect(palettenodes.some((node) => node.kind === "click" && node.category === "actions")).toBe(true);
|
|
107
|
+
expect(palettenodes.some((node) => node.kind === "loop" && node.category === "controlflow")).toBe(true);
|
|
108
|
+
expect(palettenodes.some((node) => node.kind === "delay" && node.category === "waits")).toBe(true);
|
|
109
|
+
expect(palettenodes.some((node) => node.kind === "compute" && node.category === "variables")).toBe(true);
|
|
110
|
+
expect(palettenodes.some((node) => node.kind === "cronrule" && node.category === "triggers")).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("builds the step library over every reviewed kind grouped by category", () => {
|
|
114
|
+
const kinds = ["click", "loop", "delay", "compute", "cronrule", "click"];
|
|
115
|
+
const library = buildsteplibrary(kinds);
|
|
116
|
+
expect(library.map((entry) => entry.kind)).toEqual(["click", "compute", "cronrule", "delay", "loop"]);
|
|
117
|
+
expect(library.find((entry) => entry.kind === "loop")?.category).toBe("controlflow");
|
|
118
|
+
expect(library.find((entry) => entry.kind === "delay")?.optionschema[0]).toEqual({
|
|
119
|
+
name: "base",
|
|
120
|
+
kind: "number",
|
|
121
|
+
required: true,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("editor load and save round trips", () => {
|
|
127
|
+
it("loads a composed record into the canvas and saves it back through the full grammar", () => {
|
|
128
|
+
const record = composedworkflow();
|
|
129
|
+
const model = loadworkflow(record);
|
|
130
|
+
expect(model.nodes.map((node) => node.step?.id ?? node.invocation?.block)).toEqual(["s1", "inner", "s2"]);
|
|
131
|
+
expect(model.blocks[0]?.name).toBe("inner");
|
|
132
|
+
expect(model.edges).toEqual([{ from: "i1", to: "s2", variable: "heading", kind: "string" }]);
|
|
133
|
+
const saved = saveworkflow(model, { now, kindallowed, riskof });
|
|
134
|
+
expect(saved.id).toBe("wf1");
|
|
135
|
+
expect(saved.name).toBe("roundtrip");
|
|
136
|
+
expect(saved.version).toBe(3);
|
|
137
|
+
expect(saved.risk).toBe("read");
|
|
138
|
+
expect(saved.steps.map((step) => step.id)).toEqual(["s1", "i1", "i2", "s2"]);
|
|
139
|
+
expect(saved.steps[1]?.block).toBe("inner");
|
|
140
|
+
expect(saved.steps[2]?.block).toBe("inner");
|
|
141
|
+
expect(saved.steps[3]?.bindings).toEqual([{ variable: "heading", kind: "string", stepid: "i1" }]);
|
|
142
|
+
expect(saved.blocks[0]?.steps.map((entry) => (entry as { id: string }).id)).toEqual(["i1", "i2"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("gives every repeated invocation of one block a unique canvas node id", () => {
|
|
146
|
+
const inner = workflowblockof({
|
|
147
|
+
name: "inner",
|
|
148
|
+
label: "Inner block",
|
|
149
|
+
steps: [{ id: "i1", kind: "wait", label: "Inner wait", value: "5" }],
|
|
150
|
+
});
|
|
151
|
+
if (!inner) throw new Error("The inner block did not normalize.");
|
|
152
|
+
const record = composeworkflow({
|
|
153
|
+
id: "wf2",
|
|
154
|
+
name: "twice",
|
|
155
|
+
version: 1,
|
|
156
|
+
origins: ["https://example.com"],
|
|
157
|
+
steps: [
|
|
158
|
+
{ block: "inner", label: "First" },
|
|
159
|
+
{ id: "s1", kind: "wait", label: "Between", value: "1" },
|
|
160
|
+
{ block: "inner", label: "Second" },
|
|
161
|
+
],
|
|
162
|
+
blocks: [inner],
|
|
163
|
+
now,
|
|
164
|
+
kindallowed,
|
|
165
|
+
riskof,
|
|
166
|
+
});
|
|
167
|
+
const model = loadworkflow(record);
|
|
168
|
+
expect(model.nodes.map((node) => node.id ?? node.step?.id)).toEqual(["inner", "s1", "inner2"]);
|
|
169
|
+
const saved = saveworkflow(model, { now, kindallowed, riskof });
|
|
170
|
+
expect(saved.steps.map((step) => step.id)).toEqual(["i1", "s1", "i1"]);
|
|
171
|
+
expect(saved.steps.filter((step) => step.block === "inner")).toHaveLength(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("refuses saves with duplicate node ids, unknown blocks, backwards edges and unknown edge endpoints", () => {
|
|
175
|
+
const record = composedworkflow();
|
|
176
|
+
const model = loadworkflow(record);
|
|
177
|
+
const first = model.nodes[0] as { step?: { id: string } };
|
|
178
|
+
const duplicated: editormodel = {
|
|
179
|
+
...model,
|
|
180
|
+
nodes: [...model.nodes, { step: { ...(first.step as object), id: "s1" } as never, x: 40, y: 500 }],
|
|
181
|
+
};
|
|
182
|
+
expect(() => saveworkflow(duplicated, { now, kindallowed, riskof })).toThrow("unique");
|
|
183
|
+
const attached = workflowstepof({ id: "s9", kind: "wait", label: "Attached" });
|
|
184
|
+
if (!attached) throw new Error("The attached step did not normalize.");
|
|
185
|
+
const unknownblock: editormodel = {
|
|
186
|
+
...model,
|
|
187
|
+
nodes: [...model.nodes, { step: { ...attached, block: "ghost" }, x: 40, y: 500 }],
|
|
188
|
+
};
|
|
189
|
+
expect(() => saveworkflow(unknownblock, { now, kindallowed, riskof })).toThrow("unknown block");
|
|
190
|
+
const backwards: editormodel = {
|
|
191
|
+
...model,
|
|
192
|
+
edges: [...model.edges, { from: "s2", to: "s1", variable: "late", kind: "string" }],
|
|
193
|
+
};
|
|
194
|
+
expect(() => saveworkflow(backwards, { now, kindallowed, riskof })).toThrow("cycle");
|
|
195
|
+
const dangling: editormodel = {
|
|
196
|
+
...model,
|
|
197
|
+
edges: [...model.edges, { from: "ghost", to: "s2", variable: "x", kind: "string" }],
|
|
198
|
+
};
|
|
199
|
+
expect(() => saveworkflow(dangling, { now, kindallowed, riskof })).toThrow("unknown source step");
|
|
200
|
+
expect(() => saveworkflow({ ...model, name: " " }, { now, kindallowed, riskof })).toThrow("name");
|
|
201
|
+
expect(() => saveworkflow({ ...model, version: 0 }, { now, kindallowed, riskof })).toThrow("version");
|
|
202
|
+
expect(() => saveworkflow({ ...model, origins: [] }, { now, kindallowed, riskof })).toThrow("origin");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("keeps a saved canvas reopenable through a stored layout", () => {
|
|
206
|
+
const record = composedworkflow();
|
|
207
|
+
const layout: editorlayout = { width: 900, height: 700, viewportx: 30, viewporty: 60, zoom: 1 };
|
|
208
|
+
const model = loadworkflow(record, layout);
|
|
209
|
+
expect(model.layout).toEqual({ width: 900, height: 700, viewportx: 30, viewporty: 60, zoom: 1 });
|
|
210
|
+
const reopened = loadworkflow(saveworkflow(model, { now, kindallowed, riskof }), layout);
|
|
211
|
+
expect(reopened.nodes).toHaveLength(3);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("editor canvas edits", () => {
|
|
216
|
+
it("reorders nodes and groups a selection into a new block", () => {
|
|
217
|
+
const model = loadworkflow(composedworkflow());
|
|
218
|
+
const reordered = reordersteps(model, "s2", 0);
|
|
219
|
+
expect(reordered.nodes.map((node) => node.id ?? node.step?.id)).toEqual(["s2", "s1", "inner"]);
|
|
220
|
+
expect(() => reordersteps(model, "s1", 99)).toThrow("existing position");
|
|
221
|
+
const grouped = groupselect(model, ["s1"], "wrap");
|
|
222
|
+
expect(grouped.nodes.map((node) => node.id ?? node.step?.id)).toEqual(["wrap", "inner", "s2"]);
|
|
223
|
+
expect(grouped.blocks.map((block) => block.name)).toContain("wrap");
|
|
224
|
+
const saved = saveworkflow(grouped, { now, kindallowed, riskof });
|
|
225
|
+
expect(saved.steps.map((step) => step.block)).toEqual(["wrap", "inner", "inner", undefined]);
|
|
226
|
+
expect(() => groupselect(model, ["s1"], "inner")).toThrow("already exists");
|
|
227
|
+
expect(() => groupselect(model, ["s1"], "Bad")).toThrow("lowercase word");
|
|
228
|
+
expect(() => groupselect(model, ["inner"], "wrap2")).toThrow("step nodes");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("snaps a dragged node onto the grid and attaches it to a block column", () => {
|
|
232
|
+
const model = loadworkflow(composedworkflow());
|
|
233
|
+
const snapped = snapnode(model, "s2", 347, 129, 20);
|
|
234
|
+
const moved = snapped.nodes.find((node) => node.step?.id === "s2");
|
|
235
|
+
expect(moved?.x).toBe(340);
|
|
236
|
+
expect(moved?.y).toBe(120);
|
|
237
|
+
const saved = saveworkflow(snapped, { now, kindallowed, riskof });
|
|
238
|
+
expect(saved.steps.find((step) => step.id === "s2")?.block).toBe("inner");
|
|
239
|
+
expect(() => snapnode(model, "ghost", 10, 10)).toThrow("No canvas node");
|
|
240
|
+
expect(() => snapnode(model, "inner", 10, 10)).toThrow("block invocation");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("inserts nodes from the palette and edits them through the inspector with undo coverage", () => {
|
|
244
|
+
const model = loadworkflow(composedworkflow());
|
|
245
|
+
const withnode = addnode(model, { id: "readtext", kind: "readtext", label: "Palette drop", target: "h2" });
|
|
246
|
+
expect(withnode.nodes.map((node) => node.step?.id)).toContain("readtext");
|
|
247
|
+
const again = addnode(withnode, { id: "readtext", kind: "readtext", label: "Palette drop", target: "h2" });
|
|
248
|
+
expect(again.nodes.map((node) => node.step?.id)).toContain("readtext2");
|
|
249
|
+
const edited = editstep(again, { id: "readtext2", kind: "readtext", label: "Renamed", target: "h3" });
|
|
250
|
+
expect(edited.nodes.find((node) => node.step?.id === "readtext2")?.step?.label).toBe("Renamed");
|
|
251
|
+
const undone = undoedit(edited);
|
|
252
|
+
expect(undone.nodes.find((node) => node.step?.id === "readtext2")?.step?.label).toBe("Palette drop");
|
|
253
|
+
const redone = redoedit(undone);
|
|
254
|
+
expect(redone.nodes.find((node) => node.step?.id === "readtext2")?.step?.label).toBe("Renamed");
|
|
255
|
+
expect(() => editstep(model, { id: "ghost", kind: "wait", label: "Ghost" })).toThrow("No canvas step");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("wires typed binding edges and removes them with the node", () => {
|
|
259
|
+
const model = loadworkflow(composedworkflow());
|
|
260
|
+
const withedge = addedge(model, { from: "s1", to: "s2", variable: "text", kind: "string", path: "details.text" });
|
|
261
|
+
expect(withedge.edges.some((edge) => edge.variable === "text")).toBe(true);
|
|
262
|
+
expect(() => addedge(model, { from: "s2", to: "s1", variable: "back", kind: "string" })).toThrow("backwards");
|
|
263
|
+
expect(() => addedge(model, { from: "s1", to: "s2", variable: "Bad", kind: "string" })).toThrow("lowercase word");
|
|
264
|
+
const removed = removeedge(withedge, "s1", "s2", "text");
|
|
265
|
+
expect(removed.edges.some((edge) => edge.variable === "text")).toBe(false);
|
|
266
|
+
expect(() => removeedge(model, "s1", "s2", "text")).toThrow("No canvas edge");
|
|
267
|
+
const pruned = removenode(model, "s2");
|
|
268
|
+
expect(pruned.nodes.map((node) => node.step?.id ?? node.invocation?.block)).toEqual(["s1", "inner"]);
|
|
269
|
+
expect(pruned.edges).toHaveLength(0);
|
|
270
|
+
const undone = undoedit(pruned);
|
|
271
|
+
expect(undone.nodes).toHaveLength(3);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("binds nested params into a block invocation of the canvas", () => {
|
|
275
|
+
const model = loadworkflow(composedworkflow());
|
|
276
|
+
const bound = bindparam(model, "inner", { name: "base", kind: "number", default: 7 });
|
|
277
|
+
expect(bound.nodes.find((node) => node.invocation?.block === "inner")?.invocation?.params).toEqual([
|
|
278
|
+
{ name: "base", kind: "number", default: 7 },
|
|
279
|
+
]);
|
|
280
|
+
const replaced = bindparam(bound, "inner", { name: "base", kind: "number", default: 9 });
|
|
281
|
+
expect(replaced.nodes.find((node) => node.invocation?.block === "inner")?.invocation?.params).toEqual([
|
|
282
|
+
{ name: "base", kind: "number", default: 9 },
|
|
283
|
+
]);
|
|
284
|
+
expect(() => bindparam(model, "ghost", { name: "base", kind: "number" })).toThrow("No block invocation");
|
|
285
|
+
expect(() => bindparam(model, "inner", { name: "Bad", kind: "number" })).toThrow("lowercase word");
|
|
286
|
+
const saved = saveworkflow(bound, { now, kindallowed, riskof });
|
|
287
|
+
expect(saved.steps.find((step) => step.id === "i1")?.params).toEqual([
|
|
288
|
+
{ name: "base", kind: "number", default: 7 },
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("inserts shared step templates with nested params onto the canvas", () => {
|
|
293
|
+
const template: steptemplate = {
|
|
294
|
+
id: "t1",
|
|
295
|
+
name: "shared wait",
|
|
296
|
+
origin: "https://example.com",
|
|
297
|
+
step: { id: "wait", kind: "wait", label: "Shared wait", value: "3" },
|
|
298
|
+
sharedat: now,
|
|
299
|
+
};
|
|
300
|
+
const model = loadworkflow(composedworkflow());
|
|
301
|
+
const inserted = expandtemplate(model, steptemplateof(template) as steptemplate, [
|
|
302
|
+
{ name: "base", kind: "number", default: 2 },
|
|
303
|
+
]);
|
|
304
|
+
expect(inserted.nodes.map((node) => node.step?.id)).toContain("wait");
|
|
305
|
+
expect(inserted.nodes.find((node) => node.step?.id === "wait")?.step?.params).toEqual([
|
|
306
|
+
{ name: "base", kind: "number", default: 2 },
|
|
307
|
+
]);
|
|
308
|
+
const twice = expandtemplate(inserted, steptemplateof(template) as steptemplate);
|
|
309
|
+
expect(twice.nodes.map((node) => node.step?.id)).toContain("wait2");
|
|
310
|
+
expect(() =>
|
|
311
|
+
expandtemplate(model, {
|
|
312
|
+
id: "t2",
|
|
313
|
+
name: "broken",
|
|
314
|
+
origin: "https://example.com",
|
|
315
|
+
step: { id: "x", kind: "wait" } as never,
|
|
316
|
+
sharedat: now,
|
|
317
|
+
}),
|
|
318
|
+
).toThrow("reviewed workflow step");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("searches steps by label, kind and variable name", () => {
|
|
322
|
+
const model = loadworkflow(composedworkflow());
|
|
323
|
+
const withedge = addedge(model, { from: "s1", to: "s2", variable: "headingtext", kind: "string" });
|
|
324
|
+
expect(searchsteps(withedge, "OPEN")).toEqual([
|
|
325
|
+
{ id: "s1", label: "Open read", kind: "readtext", matched: ["label"] },
|
|
326
|
+
]);
|
|
327
|
+
expect(searchsteps(withedge, "readtext")[0]?.matched).toEqual(["kind"]);
|
|
328
|
+
expect(searchsteps(withedge, "headingtext").map((result) => result.id)).toEqual(["s1", "s2"]);
|
|
329
|
+
expect(searchsteps(withedge, " ")).toEqual([]);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("editor mini map and zoom", () => {
|
|
334
|
+
it("projects the full canvas into the mini map and answers the viewport rectangle", () => {
|
|
335
|
+
const model = loadworkflow(composedworkflow());
|
|
336
|
+
const projection = renderminimap(model, 200, 100);
|
|
337
|
+
expect(projection.minimap.width).toBe(200);
|
|
338
|
+
expect(projection.minimap.scale).toBeGreaterThan(0);
|
|
339
|
+
expect(projection.minimap.scale).toBeLessThanOrEqual(100 / model.layout.height);
|
|
340
|
+
expect(projection.nodes).toHaveLength(3);
|
|
341
|
+
const zoomed = zoomcanvas(model, 2).model;
|
|
342
|
+
const zoomedprojection = renderminimap(zoomed, 200, 100);
|
|
343
|
+
expect(zoomedprojection.minimap.viewport.width).toBeCloseTo(
|
|
344
|
+
(model.layout.width / 2) * zoomedprojection.minimap.scale,
|
|
345
|
+
5,
|
|
346
|
+
);
|
|
347
|
+
expect(() => renderminimap(model, 0, 100)).toThrow("positive");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("jumps the canvas to a clicked mini map region inside the canvas bounds", () => {
|
|
351
|
+
const model = loadworkflow(composedworkflow());
|
|
352
|
+
const focused = minimapfocus(model, 0, 0);
|
|
353
|
+
expect(focused.layout.viewportx).toBe(0);
|
|
354
|
+
expect(focused.layout.viewporty).toBe(0);
|
|
355
|
+
const zoomed = zoomcanvas(model, 4).model;
|
|
356
|
+
const corner = minimapfocus(zoomed, 200, 100);
|
|
357
|
+
expect(corner.layout.viewportx).toBeGreaterThan(0);
|
|
358
|
+
expect(corner.layout.viewportx).toBeLessThanOrEqual(model.layout.width - model.layout.width / 4);
|
|
359
|
+
expect(() => zoomcanvas(model, 0)).toThrow("positive");
|
|
360
|
+
expect(zoomcanvas(model, 0.5).labelscale).toBe(2);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("editor version diffs", () => {
|
|
365
|
+
it("highlights added, removed and changed steps between two versions", () => {
|
|
366
|
+
const base = composedworkflow();
|
|
367
|
+
const edited = composeworkflow({
|
|
368
|
+
id: "wf1",
|
|
369
|
+
name: "roundtrip",
|
|
370
|
+
version: 4,
|
|
371
|
+
origins: ["https://example.com"],
|
|
372
|
+
steps: [
|
|
373
|
+
{ id: "s1", kind: "readtext", label: "Open read renamed", target: "p" },
|
|
374
|
+
{ block: "inner", label: "Call inner" },
|
|
375
|
+
{ id: "s3", kind: "wait", label: "Fresh wait", value: "20" },
|
|
376
|
+
],
|
|
377
|
+
blocks: [
|
|
378
|
+
workflowblockof({
|
|
379
|
+
name: "inner",
|
|
380
|
+
label: "Inner block",
|
|
381
|
+
steps: [
|
|
382
|
+
{ id: "i1", kind: "readtext", label: "Read the heading", target: "h1" },
|
|
383
|
+
{ id: "i2", kind: "wait", label: "Inner wait", value: "5" },
|
|
384
|
+
],
|
|
385
|
+
})!,
|
|
386
|
+
],
|
|
387
|
+
now,
|
|
388
|
+
kindallowed,
|
|
389
|
+
riskof,
|
|
390
|
+
});
|
|
391
|
+
const diff = diffversions(base, edited, now);
|
|
392
|
+
expect(diff.from).toBe(3);
|
|
393
|
+
expect(diff.to).toBe(4);
|
|
394
|
+
expect(diff.added.map((step) => step.stepid)).toEqual(["s3"]);
|
|
395
|
+
expect(diff.removed.map((step) => step.stepid)).toEqual(["s2"]);
|
|
396
|
+
expect(diff.changed).toEqual([{ stepid: "s1", kind: "readtext", label: "Open read renamed", changes: ["label"] }]);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("editor export, import and sharing", () => {
|
|
401
|
+
it("round trips a workflow file through json and yaml with the version metadata", () => {
|
|
402
|
+
const record = composedworkflow();
|
|
403
|
+
for (const format of ["json", "yaml"] as const) {
|
|
404
|
+
const exported = exportworkflow(record, format, "release note", now);
|
|
405
|
+
expect(exported.format).toBe(format);
|
|
406
|
+
expect(exported.file.format).toBe(workflowfileversion);
|
|
407
|
+
expect(exported.file.note).toBe("release note");
|
|
408
|
+
expect(exported.file.templates).toEqual([]);
|
|
409
|
+
const imported = importworkflow({ contents: exported.contents, format, now, kindallowed, riskof });
|
|
410
|
+
expect(imported.record.name).toBe("roundtrip");
|
|
411
|
+
expect(imported.record.steps.map((step) => step.id)).toEqual(["s1", "i1", "i2", "s2"]);
|
|
412
|
+
expect(imported.record.steps[3]?.bindings).toEqual([{ variable: "heading", kind: "string", stepid: "i1" }]);
|
|
413
|
+
expect(imported.record.reviewstate).toBe("pending");
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("packs templates into one share file and restores them on import", () => {
|
|
418
|
+
const record = composedworkflow();
|
|
419
|
+
const template: steptemplate = {
|
|
420
|
+
id: "t1",
|
|
421
|
+
name: "shared wait",
|
|
422
|
+
origin: "https://example.com",
|
|
423
|
+
step: { id: "wait", kind: "wait", label: "Shared wait", value: "3" },
|
|
424
|
+
sharedat: now,
|
|
425
|
+
};
|
|
426
|
+
const bundle = importworkflow({
|
|
427
|
+
contents: JSON.stringify({
|
|
428
|
+
format: workflowfileversion,
|
|
429
|
+
exportedat: now,
|
|
430
|
+
workflow: record,
|
|
431
|
+
templates: [template],
|
|
432
|
+
}),
|
|
433
|
+
now,
|
|
434
|
+
kindallowed,
|
|
435
|
+
riskof,
|
|
436
|
+
});
|
|
437
|
+
expect(bundle.templates).toEqual([steptemplateof(template)]);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("refuses imports that break the file contract", () => {
|
|
441
|
+
const record = composedworkflow();
|
|
442
|
+
const file = JSON.stringify({ format: 99, exportedat: now, workflow: record, templates: [] });
|
|
443
|
+
expect(() => importworkflow({ contents: file, now, kindallowed, riskof })).toThrow("reviewed format");
|
|
444
|
+
expect(() => importworkflow({ contents: "not json at all", format: "json", now, kindallowed, riskof })).toThrow();
|
|
445
|
+
expect(() =>
|
|
446
|
+
importworkflow({
|
|
447
|
+
contents: JSON.stringify({ format: 1, exportedat: now, workflow: { ...record, steps: [] }, templates: [] }),
|
|
448
|
+
now,
|
|
449
|
+
kindallowed,
|
|
450
|
+
riskof,
|
|
451
|
+
}),
|
|
452
|
+
).toThrow("at least one step");
|
|
453
|
+
expect(() =>
|
|
454
|
+
importworkflow({
|
|
455
|
+
contents: JSON.stringify({
|
|
456
|
+
format: 1,
|
|
457
|
+
exportedat: now,
|
|
458
|
+
workflow: { ...record, steps: [{ id: "x", kind: "explode", label: "Unknown kind" }] },
|
|
459
|
+
templates: [],
|
|
460
|
+
}),
|
|
461
|
+
now,
|
|
462
|
+
kindallowed,
|
|
463
|
+
riskof,
|
|
464
|
+
}),
|
|
465
|
+
).toThrow("reviewed action kind");
|
|
466
|
+
expect(() =>
|
|
467
|
+
importworkflow({
|
|
468
|
+
contents: JSON.stringify({
|
|
469
|
+
format: 1,
|
|
470
|
+
exportedat: now,
|
|
471
|
+
workflow: record,
|
|
472
|
+
templates: [{ id: "t", name: "broken" }],
|
|
473
|
+
}),
|
|
474
|
+
now,
|
|
475
|
+
kindallowed,
|
|
476
|
+
riskof,
|
|
477
|
+
}),
|
|
478
|
+
).toThrow("reviewed step");
|
|
479
|
+
expect(() =>
|
|
480
|
+
importworkflow({
|
|
481
|
+
contents: JSON.stringify({ format: 1, exportedat: now, workflow: record, templates: "no" }),
|
|
482
|
+
now,
|
|
483
|
+
kindallowed,
|
|
484
|
+
riskof,
|
|
485
|
+
}),
|
|
486
|
+
).toThrow("list");
|
|
487
|
+
expect(() =>
|
|
488
|
+
importworkflow({ contents: "format: 1\nworkflow: {}", format: "yaml", now, kindallowed, riskof }),
|
|
489
|
+
).toThrow();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("parses the documented yaml subset of scalars, sequences and mappings", () => {
|
|
493
|
+
const parsed = importworkflow({
|
|
494
|
+
contents:
|
|
495
|
+
'format: 1\nexportedat: 1800000000000\nworkflow:\n id: "wfy"\n name: "yamlflow"\n version: 1\n origins:\n - "https://example.com"\n steps:\n -\n id: "s1"\n kind: "wait"\n label: "Wait a beat"\n value: "10"\n blocks: []\n risk: "read"\n createdat: 1800000000000\ntemplates: []\n',
|
|
496
|
+
format: "yaml",
|
|
497
|
+
now,
|
|
498
|
+
kindallowed,
|
|
499
|
+
riskof,
|
|
500
|
+
});
|
|
501
|
+
expect(parsed.record.steps[0]?.value).toBe("10");
|
|
502
|
+
expect(parsed.record.origins).toEqual(["https://example.com"]);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe("editor breakpoints and debug runs", () => {
|
|
507
|
+
it("plans debug segments that pause before marked steps and report the remainder", () => {
|
|
508
|
+
const record = composedworkflow();
|
|
509
|
+
const marked = markbreakpoint(loadworkflow(record), "i1");
|
|
510
|
+
const surfaces = [
|
|
511
|
+
...marked.nodes.flatMap((node) => (node.step !== undefined ? [node.step] : [])),
|
|
512
|
+
...marked.blocks.flatMap((block) =>
|
|
513
|
+
block.steps.flatMap((entry) =>
|
|
514
|
+
"kind" in entry && "label" in entry && !("block" in entry)
|
|
515
|
+
? [entry as { id: string; breakpoint?: boolean }]
|
|
516
|
+
: [],
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
];
|
|
520
|
+
const breakpoints = surfaces.filter((step) => step.breakpoint === true).map((step) => step.id);
|
|
521
|
+
expect(breakpoints).toEqual(["i1"]);
|
|
522
|
+
expect(() => markbreakpoint(marked, "ghost")).toThrow("No canvas step");
|
|
523
|
+
const cleared = markbreakpoint(marked, "i1");
|
|
524
|
+
expect(cleared.blocks[0]?.steps.some((entry) => "breakpoint" in entry && entry.breakpoint === true)).toBe(false);
|
|
525
|
+
const segment = runtobreakpoint({ record, cursor: 0, breakpoints });
|
|
526
|
+
expect(segment.until).toBe(1);
|
|
527
|
+
expect(segment.pausat).toBe("i1");
|
|
528
|
+
expect(segment.remaining).toBe(3);
|
|
529
|
+
const tail = runtobreakpoint({ record, cursor: segment.until + 1, breakpoints });
|
|
530
|
+
expect(tail.until).toBe(4);
|
|
531
|
+
expect(tail.pausat).toBeUndefined();
|
|
532
|
+
expect(tail.remaining).toBe(0);
|
|
533
|
+
expect(runtobreakpoint({ record, cursor: 0, breakpoints: [] }).until).toBe(4);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("runs the segment before a breakpoint, pauses and resumes exactly there", async () => {
|
|
537
|
+
const record = composedworkflow();
|
|
538
|
+
const run = newworkflowrun({ id: "debugrun", workflowid: record.id, now });
|
|
539
|
+
const executed: string[] = [];
|
|
540
|
+
const executor = {
|
|
541
|
+
execute: async (step: { id: string }) => {
|
|
542
|
+
executed.push(step.id);
|
|
543
|
+
return { ok: true, summary: `did ${step.id}` };
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
const segment = runtobreakpoint({ record, cursor: 0, breakpoints: ["i1"] });
|
|
547
|
+
const segmentrecord = { ...record, steps: record.steps.slice(0, segment.until) };
|
|
548
|
+
const partial = await runworkflow({
|
|
549
|
+
record: segmentrecord,
|
|
550
|
+
run,
|
|
551
|
+
...executor,
|
|
552
|
+
now,
|
|
553
|
+
gates: { sessionactive: true, planapproved: true, origingranted: () => true },
|
|
554
|
+
});
|
|
555
|
+
expect(partial.run.cursor).toBe(1);
|
|
556
|
+
expect(partial.run.state).toBe("done");
|
|
557
|
+
const paused = { ...run, state: "paused" as const, cursor: segment.until, pausedat: now };
|
|
558
|
+
const resumed = await runworkflow({
|
|
559
|
+
record,
|
|
560
|
+
run: paused,
|
|
561
|
+
log: partial.log,
|
|
562
|
+
outputs: partial.outputs,
|
|
563
|
+
...executor,
|
|
564
|
+
now,
|
|
565
|
+
gates: { sessionactive: true, planapproved: true, origingranted: () => true },
|
|
566
|
+
});
|
|
567
|
+
expect(resumed.run.state).toBe("done");
|
|
568
|
+
expect(resumed.run.cursor).toBe(4);
|
|
569
|
+
expect(executed).toEqual(["s1", "i1", "i2", "s2"]);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("editor policy gates", () => {
|
|
574
|
+
it("gates editor saves on the session, the approved plan and the canvas shape", () => {
|
|
575
|
+
const model = loadworkflow(composedworkflow());
|
|
576
|
+
const session = {
|
|
577
|
+
id: "sess",
|
|
578
|
+
tabid: 4,
|
|
579
|
+
origin: "https://example.com",
|
|
580
|
+
startedat: now - 1000,
|
|
581
|
+
expiresat: now + 600_000,
|
|
582
|
+
grants: ["https://example.com"],
|
|
583
|
+
};
|
|
584
|
+
const plan = {
|
|
585
|
+
id: "run",
|
|
586
|
+
objective: "Edit the workflow",
|
|
587
|
+
origin: "https://example.com",
|
|
588
|
+
steps: [],
|
|
589
|
+
createdat: now - 2000,
|
|
590
|
+
expiresat: now + 600_000,
|
|
591
|
+
state: "approved" as const,
|
|
592
|
+
};
|
|
593
|
+
expect(editorsavegate({ session, plan, model, now }).allowed).toBe(true);
|
|
594
|
+
expect(editorsavegate({ session: undefined, plan, model, now }).allowed).toBe(false);
|
|
595
|
+
expect(editorsavegate({ session, plan: { ...plan, state: "pending" as never }, model, now }).allowed).toBe(false);
|
|
596
|
+
expect(editorsavegate({ session, plan, model: { ...model, name: " " }, now }).allowed).toBe(false);
|
|
597
|
+
const firstnode = model.nodes[0] as editormodel["nodes"][number];
|
|
598
|
+
const duplicated = { ...model, nodes: [...model.nodes, { ...firstnode }] };
|
|
599
|
+
expect(editorsavegate({ session, plan, model: duplicated, now }).allowed).toBe(false);
|
|
600
|
+
const backwards: editormodel = {
|
|
601
|
+
...model,
|
|
602
|
+
edges: [...model.edges, { from: "s2", to: "s1", variable: "late", kind: "string" }],
|
|
603
|
+
};
|
|
604
|
+
expect(editorsavegate({ session, plan, model: backwards, now }).reason ?? "").toContain("cycle");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("grades imported workflows and rollbacks unreviewed until the user approves them", () => {
|
|
608
|
+
const record = composedworkflow();
|
|
609
|
+
expect(runreviewgranted({ ...record, reviewstate: "pending" }).allowed).toBe(false);
|
|
610
|
+
expect(runreviewgranted(record).allowed).toBe(true);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("validates per site overrides against the reviewed knobs and export contents against secrets", () => {
|
|
614
|
+
expect(validatesiteoverride({ pattern: "https://example.com", deltas: { loopbound: 20 } }).allowed).toBe(true);
|
|
615
|
+
expect(validatesiteoverride({ pattern: "https://*.example.com", deltas: { waitms: 500 } }).allowed).toBe(true);
|
|
616
|
+
expect(validatesiteoverride({ pattern: "https://example.com/path", deltas: {} }).allowed).toBe(false);
|
|
617
|
+
expect(validatesiteoverride({ pattern: "http://example.com", deltas: {} }).allowed).toBe(false);
|
|
618
|
+
expect(validatesiteoverride({ pattern: "https://example.com", deltas: { speed: 2 } }).allowed).toBe(false);
|
|
619
|
+
expect(validatesiteoverride({ pattern: "https://example.com", deltas: { loopbound: -1 } }).allowed).toBe(false);
|
|
620
|
+
const clean = composedworkflow();
|
|
621
|
+
expect(exportcontentreview({ workflow: clean, templates: [] }).allowed).toBe(true);
|
|
622
|
+
const plainstep = workflowstepof({ id: "s9", kind: "wait", label: "Wait" });
|
|
623
|
+
if (!plainstep) throw new Error("The secret step did not normalize.");
|
|
624
|
+
const secretstep: workflowrecord = {
|
|
625
|
+
...clean,
|
|
626
|
+
steps: [...clean.steps, { ...plainstep, options: '{"apikey":"abc"}' }],
|
|
627
|
+
};
|
|
628
|
+
expect(exportcontentreview({ workflow: secretstep, templates: [] }).allowed).toBe(false);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("bounds the watchdog configuration as user configured values", () => {
|
|
632
|
+
expect(watchdogconfigvalid({ enabled: true, stallthreshold: 30_000, action: "pause" }).allowed).toBe(true);
|
|
633
|
+
expect(
|
|
634
|
+
watchdogconfigvalid({ enabled: true, stallthreshold: 30_000, action: "pause", zombiewindow: 60_000 }).allowed,
|
|
635
|
+
).toBe(true);
|
|
636
|
+
expect(watchdogconfigvalid({ enabled: true, stallthreshold: 0, action: "pause" }).allowed).toBe(false);
|
|
637
|
+
expect(watchdogconfigvalid({ enabled: true, stallthreshold: 10, action: "explode" as never }).allowed).toBe(false);
|
|
638
|
+
expect(watchdogconfigvalid({ enabled: "yes" as never, stallthreshold: 10, action: "pause" }).allowed).toBe(false);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
describe("per site override application", () => {
|
|
643
|
+
it("applies reviewed knob deltas to the matching steps of a workflow", () => {
|
|
644
|
+
const record = composeworkflow({
|
|
645
|
+
id: "wf3",
|
|
646
|
+
name: "knobs",
|
|
647
|
+
version: 1,
|
|
648
|
+
origins: ["https://example.com"],
|
|
649
|
+
steps: [
|
|
650
|
+
{ id: "d1", kind: "delay", label: "Delay", options: '{"base":100}' },
|
|
651
|
+
{ id: "w1", kind: "waitelement", label: "Wait element", target: "h1", options: '{"timeout":1000}' },
|
|
652
|
+
],
|
|
653
|
+
now,
|
|
654
|
+
kindallowed,
|
|
655
|
+
riskof,
|
|
656
|
+
});
|
|
657
|
+
const overridden = applyoverride(record, {
|
|
658
|
+
id: "o1",
|
|
659
|
+
workflowid: record.id,
|
|
660
|
+
pattern: "https://example.com",
|
|
661
|
+
deltas: { delaybase: 250, waitms: 900 },
|
|
662
|
+
createdat: now,
|
|
663
|
+
});
|
|
664
|
+
expect(overridden.steps[0]?.options).toBe('{"base":250}');
|
|
665
|
+
expect(overridden.steps[1]?.options).toBe('{"timeout":900}');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("refuses patterns that match no workflow origin and knobs outside the reviewed set", () => {
|
|
669
|
+
const record = composedworkflow();
|
|
670
|
+
expect(() =>
|
|
671
|
+
applyoverride(record, {
|
|
672
|
+
id: "o1",
|
|
673
|
+
workflowid: record.id,
|
|
674
|
+
pattern: "https://other.example",
|
|
675
|
+
deltas: { delaybase: 250 },
|
|
676
|
+
createdat: now,
|
|
677
|
+
}),
|
|
678
|
+
).toThrow("matches none");
|
|
679
|
+
expect(() =>
|
|
680
|
+
applyoverride(record, {
|
|
681
|
+
id: "o1",
|
|
682
|
+
workflowid: record.id,
|
|
683
|
+
pattern: "https://example.com",
|
|
684
|
+
deltas: { speed: 2 },
|
|
685
|
+
createdat: now,
|
|
686
|
+
}),
|
|
687
|
+
).toThrow("reviewed knobs");
|
|
688
|
+
expect(() =>
|
|
689
|
+
applyoverride(record, {
|
|
690
|
+
id: "o1",
|
|
691
|
+
workflowid: record.id,
|
|
692
|
+
pattern: "https://example.com",
|
|
693
|
+
deltas: { delaybase: -5 },
|
|
694
|
+
createdat: now,
|
|
695
|
+
}),
|
|
696
|
+
).toThrow("positive number");
|
|
697
|
+
const delayrecord = composeworkflow({
|
|
698
|
+
id: "wf4",
|
|
699
|
+
name: "delays",
|
|
700
|
+
version: 1,
|
|
701
|
+
origins: ["https://sub.example.com"],
|
|
702
|
+
steps: [{ id: "d1", kind: "delay", label: "Delay", options: '{"base":100}' }],
|
|
703
|
+
now,
|
|
704
|
+
kindallowed,
|
|
705
|
+
riskof,
|
|
706
|
+
});
|
|
707
|
+
const overridden = applyoverride(delayrecord, {
|
|
708
|
+
id: "o2",
|
|
709
|
+
workflowid: delayrecord.id,
|
|
710
|
+
pattern: "https://*.example.com",
|
|
711
|
+
deltas: { delaybase: 400 },
|
|
712
|
+
createdat: now,
|
|
713
|
+
});
|
|
714
|
+
expect(overridden.steps[0]?.options).toBe('{"base":400}');
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe("the editor context feed the sidepanel renders", () => {
|
|
719
|
+
it("ships the flat editor view the renderworkfloweditor surface reads, never the protocol envelope", async () => {
|
|
720
|
+
/* the 2.0.8 end-to-end run caught the context handing renderworkfloweditor the protocol envelope of editorstate — the wrapper carries its view under an inner editor field, so the flat read of versions answered `undefined` and the length access aborted every refresh mid-render; the context now unwraps the envelope at the source */
|
|
721
|
+
const background = await readFile("background.ts", "utf8");
|
|
722
|
+
expect(background).toContain("listwatchdogevents() } }).editor,");
|
|
723
|
+
/* the flat web root carries the extension surfaces beside the spa — the editor context feed reads the flat module */
|
|
724
|
+
const sidepanel = await readFile("web/sidepanel.ts", "utf8");
|
|
725
|
+
expect(sidepanel).toContain("const editor = context.editor;");
|
|
726
|
+
expect(sidepanel).toContain("editor?.versions.length ?? 0");
|
|
727
|
+
const envelope = editorstate({
|
|
728
|
+
versions: [],
|
|
729
|
+
history: [],
|
|
730
|
+
overrides: [],
|
|
731
|
+
imports: [],
|
|
732
|
+
backgroundruns: {},
|
|
733
|
+
watchdog: { events: [] },
|
|
734
|
+
});
|
|
735
|
+
expect(envelope.editor.versions).toEqual([]);
|
|
736
|
+
expect(typeof envelope.version).toBe("string");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("answers the view flags the surface renders with the boolean grammar the commands declare", async () => {
|
|
740
|
+
/* the 2.0.8 end-to-end run caught the schemastrict registry declaring the flat view field of the perf, schedule, resilience, state and fleet families as objects while every surface request carries the boolean flag — the mismatch refused all five view commands before dispatch; the registry now answers the boolean contract */
|
|
741
|
+
const background = await readFile("background.ts", "utf8");
|
|
742
|
+
for (const anchor of [
|
|
743
|
+
'export: "object", view: "boolean" },',
|
|
744
|
+
'slowmo: "object", settings: "object", view: "boolean" },',
|
|
745
|
+
'summary: "object", settings: "object", view: "boolean" },',
|
|
746
|
+
'vote: "object", replay: "object", view: "boolean" },',
|
|
747
|
+
])
|
|
748
|
+
expect(background).toContain(anchor);
|
|
749
|
+
const sidepanel = await readFile("web/sidepanel.ts", "utf8");
|
|
750
|
+
for (const anchor of [
|
|
751
|
+
'{ kind: "perf", view: true }',
|
|
752
|
+
'{ kind: "schedule", view: true }',
|
|
753
|
+
'{ kind: "resilience", view: true }',
|
|
754
|
+
'{ kind: "state", view: true }',
|
|
755
|
+
'{ kind: "fleet", view: true }',
|
|
756
|
+
])
|
|
757
|
+
expect(sidepanel).toContain(anchor);
|
|
758
|
+
});
|
|
759
|
+
});
|