@wenathlan/devthink 2.0.12 → 2.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -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,240 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatdiagnostics,
|
|
4
|
+
lintplanfile,
|
|
5
|
+
loopkinds,
|
|
6
|
+
parseplanfile,
|
|
7
|
+
planlintexitcode,
|
|
8
|
+
planlintsummary,
|
|
9
|
+
planoriginprofile,
|
|
10
|
+
plansensitiveclasses,
|
|
11
|
+
retrykinds,
|
|
12
|
+
rulesetcachekey,
|
|
13
|
+
} from "../plan.js";
|
|
14
|
+
import { actionkindcatalog, portablerulesetgate, portablerulesetof, portablerulefamilies } from "../policy.js";
|
|
15
|
+
import { packageversion } from "../version.js";
|
|
16
|
+
import type { classconsent, planfile, planlintdiagnostic, portableruleset } from "../types.js";
|
|
17
|
+
|
|
18
|
+
const now = 1_800_000_000_000;
|
|
19
|
+
const ruleset = portablerulesetof(now);
|
|
20
|
+
const capabilities = actionkindcatalog();
|
|
21
|
+
|
|
22
|
+
function file(overrides?: Partial<planfile>): planfile {
|
|
23
|
+
return {
|
|
24
|
+
version: packageversion,
|
|
25
|
+
goal: "digest the changelog of the release",
|
|
26
|
+
origin: "https://example.org",
|
|
27
|
+
steps: [
|
|
28
|
+
{ id: "observe", kind: "observe", label: "Observe the page" },
|
|
29
|
+
{ id: "read", kind: "readtext", label: "Read the changelog", target: "main" },
|
|
30
|
+
],
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("planlint of plan files", () => {
|
|
36
|
+
it("parses one plan file under schemastrict and refuses unknown fields with the path that names them", () => {
|
|
37
|
+
const parsed = parseplanfile(file());
|
|
38
|
+
expect(parsed.origin).toBe("https://example.org");
|
|
39
|
+
expect(parsed.steps).toHaveLength(2);
|
|
40
|
+
expect(() => parseplanfile({ ...file(), unexpected: true })).toThrow(/unknown field.*unexpected.*schemastrict/);
|
|
41
|
+
const firststep = file().steps[0];
|
|
42
|
+
expect(firststep).toBeDefined();
|
|
43
|
+
expect(() => parseplanfile({ ...file(), steps: [{ ...(firststep as object), extra: 1 } as object] })).toThrow(
|
|
44
|
+
/unknown field.*extra/,
|
|
45
|
+
);
|
|
46
|
+
expect(() => parseplanfile({ ...file(), origin: "http://example.org" })).toThrow(/HTTPS/);
|
|
47
|
+
expect(() => parseplanfile({ ...file(), steps: [] })).toThrow(/at least one step/);
|
|
48
|
+
expect(() => parseplanfile({ ...file(), steps: [{ id: "s", kind: "loop", label: "l", bound: 0 }] })).toThrow(
|
|
49
|
+
/positive integer/,
|
|
50
|
+
);
|
|
51
|
+
expect(() =>
|
|
52
|
+
parseplanfile({ ...file(), steps: [{ id: "s", kind: "retryaction", label: "l", attempts: -2 }] }),
|
|
53
|
+
).toThrow(/positive integer/);
|
|
54
|
+
expect(() => parseplanfile({ ...file(), grants: [""] })).toThrow(/grants/);
|
|
55
|
+
expect(() => parseplanfile({ ...file(), denials: ["click", 3] })).toThrow(/denials/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps one clean read only plan free of diagnostics", () => {
|
|
59
|
+
expect(lintplanfile({ file: file(), ruleset, capabilities, now })).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("applies the same originprofile rules as the extension: a denied kind never lints clean", () => {
|
|
63
|
+
const denied = lintplanfile({
|
|
64
|
+
file: file({
|
|
65
|
+
steps: [{ id: "type", kind: "type", label: "Type the query", target: "input", gate: true }],
|
|
66
|
+
denials: ["type"],
|
|
67
|
+
}),
|
|
68
|
+
ruleset,
|
|
69
|
+
capabilities,
|
|
70
|
+
now,
|
|
71
|
+
});
|
|
72
|
+
const grade = denied.find((diagnostic) => diagnostic.code === "plan.origin.grade");
|
|
73
|
+
expect(grade?.severity).toBe("error");
|
|
74
|
+
expect(grade?.path).toBe("steps[0].kind");
|
|
75
|
+
expect(grade?.message).toMatch(/denies the type kind/);
|
|
76
|
+
expect(planoriginprofile(file({ denials: ["type"] }), now).denials).toEqual(["type"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("demands the same fresh class consents the extension demands", () => {
|
|
80
|
+
const paying = file({ steps: [{ id: "card", kind: "fillcard", label: "Fill the card", gate: true }] });
|
|
81
|
+
const withoutconsent = lintplanfile({ file: paying, ruleset, capabilities, now });
|
|
82
|
+
expect(
|
|
83
|
+
withoutconsent.some(
|
|
84
|
+
(diagnostic) => diagnostic.code === "plan.consent.class" && diagnostic.message.includes("payment"),
|
|
85
|
+
),
|
|
86
|
+
).toBe(true);
|
|
87
|
+
const consent: classconsent = {
|
|
88
|
+
id: "c1",
|
|
89
|
+
origin: "https://example.org",
|
|
90
|
+
sensitiveclass: "payment",
|
|
91
|
+
grantedat: now - 1000,
|
|
92
|
+
} as classconsent;
|
|
93
|
+
const withconsent = lintplanfile({ file: paying, ruleset, capabilities, consents: [consent], now });
|
|
94
|
+
expect(withconsent.some((diagnostic) => diagnostic.code === "plan.consent.class")).toBe(false);
|
|
95
|
+
expect(withconsent.some((diagnostic) => diagnostic.code === "plan.gate.declaration")).toBe(false);
|
|
96
|
+
expect(plansensitiveclasses(paying)).toEqual(["payment"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("flags steps whose kinds exceed the portable capability set", () => {
|
|
100
|
+
const diagnostics = lintplanfile({ file: file(), ruleset, capabilities: ["observe"], now });
|
|
101
|
+
const capability = diagnostics.find((diagnostic) => diagnostic.code === "plan.capability.kind");
|
|
102
|
+
expect(capability?.severity).toBe("error");
|
|
103
|
+
expect(capability?.message).toMatch(/readtext.*exceeds the portable capability set/);
|
|
104
|
+
expect(lintplanfile({ file: file(), ruleset, capabilities: ["observe", "readtext"], now })).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("flags sensitive steps without an explicit gate declaration", () => {
|
|
108
|
+
const typing = file({ steps: [{ id: "type", kind: "type", label: "Type the query", target: "input" }] });
|
|
109
|
+
const ungated = lintplanfile({ file: typing, ruleset, capabilities, now });
|
|
110
|
+
expect(
|
|
111
|
+
ungated.some((diagnostic) => diagnostic.code === "plan.gate.declaration" && diagnostic.severity === "error"),
|
|
112
|
+
).toBe(true);
|
|
113
|
+
const gated = lintplanfile({
|
|
114
|
+
file: file({ steps: [{ id: "type", kind: "type", label: "Type the query", target: "input", gate: true }] }),
|
|
115
|
+
ruleset,
|
|
116
|
+
capabilities,
|
|
117
|
+
now,
|
|
118
|
+
});
|
|
119
|
+
expect(gated.some((diagnostic) => diagnostic.code === "plan.gate.declaration")).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("flags selectors that cannot resolve without a live page as info", () => {
|
|
123
|
+
const templated = lintplanfile({
|
|
124
|
+
file: file({
|
|
125
|
+
steps: [{ id: "click", kind: "click", label: "Click the hit", target: "{{firsthit}}", gate: true }],
|
|
126
|
+
}),
|
|
127
|
+
ruleset,
|
|
128
|
+
capabilities,
|
|
129
|
+
now,
|
|
130
|
+
});
|
|
131
|
+
const selector = templated.find((diagnostic) => diagnostic.code === "plan.selector.static");
|
|
132
|
+
expect(selector?.severity).toBe("info");
|
|
133
|
+
expect(selector?.message).toMatch(/resolves only against the live page/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("flags unbounded loops and missing retry bounds as warnings", () => {
|
|
137
|
+
expect(loopkinds).toContain("loop");
|
|
138
|
+
expect(retrykinds).toContain("retryaction");
|
|
139
|
+
const unbounded = lintplanfile({
|
|
140
|
+
file: file({ steps: [{ id: "spin", kind: "loop", label: "Spin the rows" }] }),
|
|
141
|
+
ruleset,
|
|
142
|
+
capabilities,
|
|
143
|
+
now,
|
|
144
|
+
});
|
|
145
|
+
expect(
|
|
146
|
+
unbounded.some((diagnostic) => diagnostic.code === "plan.control.loopbound" && diagnostic.severity === "warn"),
|
|
147
|
+
).toBe(true);
|
|
148
|
+
const unretrying = lintplanfile({
|
|
149
|
+
file: file({ steps: [{ id: "retry", kind: "retryaction", label: "Retry the click" }] }),
|
|
150
|
+
ruleset,
|
|
151
|
+
capabilities,
|
|
152
|
+
now,
|
|
153
|
+
});
|
|
154
|
+
expect(
|
|
155
|
+
unretrying.some((diagnostic) => diagnostic.code === "plan.control.retrybound" && diagnostic.severity === "warn"),
|
|
156
|
+
).toBe(true);
|
|
157
|
+
const bounded = lintplanfile({
|
|
158
|
+
file: file({
|
|
159
|
+
steps: [
|
|
160
|
+
{ id: "spin", kind: "loop", label: "Spin the rows", bound: 12 },
|
|
161
|
+
{ id: "retry", kind: "retryaction", label: "Retry the click", attempts: 3 },
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
164
|
+
ruleset,
|
|
165
|
+
capabilities,
|
|
166
|
+
now,
|
|
167
|
+
});
|
|
168
|
+
expect(bounded.some((diagnostic) => diagnostic.code.startsWith("plan.control."))).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("runs only the rules the compiled set carries so a cache from another release never lints half", () => {
|
|
172
|
+
const typing = file({ steps: [{ id: "type", kind: "type", label: "Type the query", target: "input" }] });
|
|
173
|
+
const withoutgate = { ...ruleset, rules: ruleset.rules.filter((rule) => rule.id !== "gatedeclaration") };
|
|
174
|
+
expect(
|
|
175
|
+
lintplanfile({ file: typing, ruleset: withoutgate, capabilities, now }).some(
|
|
176
|
+
(diagnostic) => diagnostic.code === "plan.gate.declaration",
|
|
177
|
+
),
|
|
178
|
+
).toBe(false);
|
|
179
|
+
expect(
|
|
180
|
+
lintplanfile({ file: typing, ruleset, capabilities, now }).some(
|
|
181
|
+
(diagnostic) => diagnostic.code === "plan.gate.declaration",
|
|
182
|
+
),
|
|
183
|
+
).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("formats diagnostics in human and json shapes and exits non zero on error only", () => {
|
|
187
|
+
const diagnostics: planlintdiagnostic[] = [
|
|
188
|
+
{
|
|
189
|
+
code: "plan.selector.static",
|
|
190
|
+
path: "steps[0].target",
|
|
191
|
+
severity: "info",
|
|
192
|
+
message: "A templated selector resolves only against the live page.",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
code: "plan.control.loopbound",
|
|
196
|
+
path: "steps[1].bound",
|
|
197
|
+
severity: "warn",
|
|
198
|
+
message: "The loop carries no bound.",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
code: "plan.gate.declaration",
|
|
202
|
+
path: "steps[2].gate",
|
|
203
|
+
severity: "error",
|
|
204
|
+
message: "A sensitive step declares no gate.",
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
expect(planlintexitcode(diagnostics.slice(0, 2))).toBe(0);
|
|
208
|
+
expect(planlintexitcode(diagnostics)).toBe(1);
|
|
209
|
+
expect(formatdiagnostics([], "human")).toBe("No planlint diagnostics.");
|
|
210
|
+
expect(formatdiagnostics(diagnostics, "human")).toContain("ERROR plan.gate.declaration at steps[2].gate");
|
|
211
|
+
expect(JSON.parse(formatdiagnostics(diagnostics, "json"))).toHaveLength(3);
|
|
212
|
+
const summary = planlintsummary(diagnostics);
|
|
213
|
+
expect(summary).toMatchObject({ info: 1, warn: 1, error: 1, exitcode: 1 });
|
|
214
|
+
expect(summary.reason).toMatch(/refuse the plan file/);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("the portable rule set", () => {
|
|
219
|
+
it("compiles one versioned set shared by the extension and the cli with a stable cache key", () => {
|
|
220
|
+
expect(ruleset.version).toBe(packageversion);
|
|
221
|
+
expect(ruleset.rules.map((rule) => rule.id)).toContain("originprofilegrade");
|
|
222
|
+
expect(rulesetcachekey(ruleset)).toBe(rulesetcachekey(portablerulesetof(now + 5_000)));
|
|
223
|
+
expect(rulesetcachekey({ ...ruleset, rules: ruleset.rules.slice(1) })).not.toBe(rulesetcachekey(ruleset));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("gates the compiled set against drift", () => {
|
|
227
|
+
expect(portablerulesetgate(ruleset).allowed).toBe(true);
|
|
228
|
+
expect(portablerulesetgate({ ...ruleset, version: "0.0.1" }).allowed).toBe(false);
|
|
229
|
+
expect(portablerulesetgate({ ...ruleset, rules: [] }).allowed).toBe(false);
|
|
230
|
+
expect(portablerulesetgate({ ...ruleset, rules: [...ruleset.rules, ruleset.rules[0] as never] }).allowed).toBe(
|
|
231
|
+
false,
|
|
232
|
+
);
|
|
233
|
+
const missingfamily = { ...ruleset, rules: ruleset.rules.filter((rule) => rule.family !== "consent") };
|
|
234
|
+
expect(portablerulesetgate(missingfamily).allowed).toBe(false);
|
|
235
|
+
expect(portablerulesetgate({ ...ruleset, rules: ruleset.rules.map((rule) => ({ ...rule, id: "" })) }).allowed).toBe(
|
|
236
|
+
false,
|
|
237
|
+
);
|
|
238
|
+
expect(portablerulefamilies).toHaveLength(7);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
activetimelineanchor,
|
|
4
|
+
appendlogstreamevent,
|
|
5
|
+
auditexcerptof,
|
|
6
|
+
diffpreviewof,
|
|
7
|
+
filterlogstream,
|
|
8
|
+
livebufferof,
|
|
9
|
+
loglevelof,
|
|
10
|
+
logstreameventof,
|
|
11
|
+
logstreamgenesis,
|
|
12
|
+
maskverdictsof,
|
|
13
|
+
plancardgroups,
|
|
14
|
+
plancardsof,
|
|
15
|
+
resolutionhistoryafter,
|
|
16
|
+
resolutionlogeventof,
|
|
17
|
+
stepresolutionof,
|
|
18
|
+
stepstimelinenodes,
|
|
19
|
+
verifylogstream,
|
|
20
|
+
} from "../plan.js";
|
|
21
|
+
import { diffpreviewgate, logbufferboundvalid, logstreamegressgate, stepapprovegate } from "../policy.js";
|
|
22
|
+
import type { agentplan, correctionentry, planprogress } from "../types.js";
|
|
23
|
+
|
|
24
|
+
const now = 1_800_000_000_000;
|
|
25
|
+
|
|
26
|
+
const plan: agentplan = {
|
|
27
|
+
id: "run1",
|
|
28
|
+
objective: "Fill the checkout form",
|
|
29
|
+
origin: "https://shop.example",
|
|
30
|
+
steps: [
|
|
31
|
+
{ id: "s1", kind: "readtable", risk: "read", summary: "Read the pricing table." },
|
|
32
|
+
{ id: "s2", kind: "click", risk: "interaction", target: "#buy", summary: "Open the checkout." },
|
|
33
|
+
{
|
|
34
|
+
id: "s3",
|
|
35
|
+
kind: "type",
|
|
36
|
+
risk: "sensitive",
|
|
37
|
+
target: "#card",
|
|
38
|
+
value: "vault:card1",
|
|
39
|
+
summary: "Type the card number from the vault.",
|
|
40
|
+
},
|
|
41
|
+
{ id: "s4", kind: "submit", risk: "sensitive", target: "#pay", summary: "Submit the payment." },
|
|
42
|
+
],
|
|
43
|
+
createdat: now - 5000,
|
|
44
|
+
expiresat: now + 5000,
|
|
45
|
+
state: "approved",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const corrections: correctionentry[] = [
|
|
49
|
+
{
|
|
50
|
+
id: "c1",
|
|
51
|
+
origin: "https://shop.example",
|
|
52
|
+
kind: "type",
|
|
53
|
+
stepid: "s3",
|
|
54
|
+
source: "edited",
|
|
55
|
+
original: "{kind:type,value:plain}",
|
|
56
|
+
corrected: "{kind:type,value:vault:card1}",
|
|
57
|
+
reason: "The vault marker replaced the raw value.",
|
|
58
|
+
at: now - 1000,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "c2",
|
|
62
|
+
origin: "https://other.example",
|
|
63
|
+
kind: "click",
|
|
64
|
+
stepid: "s2",
|
|
65
|
+
source: "rejected",
|
|
66
|
+
original: "{kind:click}",
|
|
67
|
+
reason: "Other origin never matches.",
|
|
68
|
+
at: now - 1000,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
describe("plan review surface", () => {
|
|
73
|
+
it("builds one plancard per proposed step with kind, risk class, environment and options", () => {
|
|
74
|
+
const cards = plancardsof({ plan, corrections });
|
|
75
|
+
expect(cards).toHaveLength(4);
|
|
76
|
+
const card = cards[2];
|
|
77
|
+
expect(card?.stepid).toBe("s3");
|
|
78
|
+
expect(card?.kind).toBe("type");
|
|
79
|
+
expect(card?.risk).toBe("sensitive");
|
|
80
|
+
expect(card?.environment).toBe("pagecontext");
|
|
81
|
+
expect(card?.corrections.map((entry) => entry.id)).toEqual(["c1"]);
|
|
82
|
+
expect(card?.editable).toBe(false);
|
|
83
|
+
const pending: agentplan = { ...plan, state: "pending" };
|
|
84
|
+
expect(plancardsof({ plan: pending, corrections: [] })[0]?.editable).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("groups the plancards by risk class with the sensitive classes expanded by default", () => {
|
|
88
|
+
const groups = plancardgroups(plancardsof({ plan, corrections }));
|
|
89
|
+
expect(groups.map((group) => group.risk)).toEqual(["sensitive", "interaction", "read"]);
|
|
90
|
+
expect(groups[0]?.cards).toHaveLength(2);
|
|
91
|
+
expect(groups[0]?.expanded).toBe(true);
|
|
92
|
+
expect(groups[1]?.expanded).toBe(false);
|
|
93
|
+
expect(groups[2]?.expanded).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("resolves one step per distinct human action with approve, reject and edit provenance", () => {
|
|
97
|
+
const approve = stepresolutionof({
|
|
98
|
+
stepid: "s2",
|
|
99
|
+
planid: plan.id,
|
|
100
|
+
origin: plan.origin,
|
|
101
|
+
resolution: "approve",
|
|
102
|
+
surface: "sidepanel",
|
|
103
|
+
at: now,
|
|
104
|
+
});
|
|
105
|
+
expect(approve.resolution).toBe("approve");
|
|
106
|
+
const edit = stepresolutionof({
|
|
107
|
+
stepid: "s3",
|
|
108
|
+
planid: plan.id,
|
|
109
|
+
origin: plan.origin,
|
|
110
|
+
resolution: "edit",
|
|
111
|
+
surface: "sidepanel",
|
|
112
|
+
edited: "{kind:type,value:vault:card1}",
|
|
113
|
+
at: now,
|
|
114
|
+
});
|
|
115
|
+
expect(edit.edited).toBe("{kind:type,value:vault:card1}");
|
|
116
|
+
expect(() =>
|
|
117
|
+
stepresolutionof({
|
|
118
|
+
stepid: "s3",
|
|
119
|
+
planid: plan.id,
|
|
120
|
+
origin: plan.origin,
|
|
121
|
+
resolution: "edit",
|
|
122
|
+
surface: "sidepanel",
|
|
123
|
+
at: now,
|
|
124
|
+
}),
|
|
125
|
+
).toThrow(/corrected step shape/);
|
|
126
|
+
expect(() =>
|
|
127
|
+
stepresolutionof({
|
|
128
|
+
stepid: " ",
|
|
129
|
+
planid: plan.id,
|
|
130
|
+
origin: plan.origin,
|
|
131
|
+
resolution: "approve",
|
|
132
|
+
surface: "sidepanel",
|
|
133
|
+
at: now,
|
|
134
|
+
}),
|
|
135
|
+
).toThrow(/step/);
|
|
136
|
+
const history = resolutionhistoryafter([approve], edit);
|
|
137
|
+
expect(history[0]?.stepid).toBe("s3");
|
|
138
|
+
const logentry = resolutionlogeventof(edit);
|
|
139
|
+
expect(logentry.kind).toBe("review");
|
|
140
|
+
expect(logentry.stepid).toBe("s3");
|
|
141
|
+
expect(logentry.summary).toMatch(/edited the step s3/);
|
|
142
|
+
expect(resolutionlogeventof(approve).summary).toMatch(/approved the step s2/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("binds each stepapprove action to one step with no batch approval and no background provenance", () => {
|
|
146
|
+
expect(stepapprovegate({ stepids: ["s2"], resolution: "approve", surface: "sidepanel" }).allowed).toBe(true);
|
|
147
|
+
expect(stepapprovegate({ stepids: ["s2", "s3"], resolution: "approve", surface: "sidepanel" }).reason).toMatch(
|
|
148
|
+
/batch of 2/,
|
|
149
|
+
);
|
|
150
|
+
expect(stepapprovegate({ stepids: [], resolution: "approve", surface: "sidepanel" }).allowed).toBe(false);
|
|
151
|
+
expect(stepapprovegate({ stepids: ["s2"], resolution: "reject", surface: "background" }).reason).toMatch(
|
|
152
|
+
/distinct human action/,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("compares the observed before state with the predicted after state of write class steps only", () => {
|
|
157
|
+
expect(diffpreviewgate({ risk: "sensitive" }).allowed).toBe(true);
|
|
158
|
+
expect(diffpreviewgate({ risk: "read" }).reason).toMatch(/write class steps only/);
|
|
159
|
+
expect(diffpreviewgate({ risk: "interaction" }).allowed).toBe(false);
|
|
160
|
+
const preview = diffpreviewof({
|
|
161
|
+
stepid: "s3",
|
|
162
|
+
before: { card: "vault:card1", holder: "Ada", city: "Lisbon" },
|
|
163
|
+
after: { card: "vault:card1", holder: "Ada Lovelace", zip: "1000" },
|
|
164
|
+
maskverdicts: { card: "The card value stays masked (11 characters) and never renders in the clear." },
|
|
165
|
+
provenance: "inline",
|
|
166
|
+
});
|
|
167
|
+
expect(preview.changes.map((change) => `${change.kind}:${change.field}`).sort()).toEqual([
|
|
168
|
+
"added:zip",
|
|
169
|
+
"changed:holder",
|
|
170
|
+
"removed:city",
|
|
171
|
+
]);
|
|
172
|
+
expect(preview.maskverdicts.card).toMatch(/masked/);
|
|
173
|
+
expect(preview.provenance).toBe("inline");
|
|
174
|
+
const verdicts = maskverdictsof({ password: "hunter2", title: "Pricing" }, ["password"]);
|
|
175
|
+
expect(verdicts.password).toMatch(/password value stays masked/);
|
|
176
|
+
expect(verdicts.title).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("derives the stepstimeline nodes from progress records with no new state", () => {
|
|
180
|
+
const progress: planprogress = {
|
|
181
|
+
planid: plan.id,
|
|
182
|
+
completedsteps: ["s1", "s2"],
|
|
183
|
+
outcomes: [{ stepid: "s2", ok: true, summary: "Checkout opened.", at: now - 500 }],
|
|
184
|
+
environments: { s1: "offscreenworker" },
|
|
185
|
+
turnarounds: { s1: 120 },
|
|
186
|
+
gatewaits: { s3: { gateid: "g1", kind: "confirmpay", openedat: now - 100, resolvedat: now, waitedms: 100 } },
|
|
187
|
+
updatedat: now,
|
|
188
|
+
};
|
|
189
|
+
const nodes = stepstimelinenodes({ plan, progress, now });
|
|
190
|
+
expect(nodes.map((node) => node.status)).toEqual(["done", "done", "waiting", "pending"]);
|
|
191
|
+
expect(nodes[0]?.environment).toBe("offscreenworker");
|
|
192
|
+
expect(nodes[0]?.durationms).toBe(120);
|
|
193
|
+
expect(nodes[0]?.resultsummary).toBeUndefined();
|
|
194
|
+
expect(nodes[1]?.resultsummary).toBe("Checkout opened.");
|
|
195
|
+
expect(nodes[2]?.status).toBe("waiting");
|
|
196
|
+
expect(activetimelineanchor(nodes)).toBeUndefined();
|
|
197
|
+
const cancelled: agentplan = { ...plan, state: "cancelled" };
|
|
198
|
+
expect(stepstimelinenodes({ plan: cancelled, now }).map((node) => node.status)).toEqual([
|
|
199
|
+
"halted",
|
|
200
|
+
"halted",
|
|
201
|
+
"halted",
|
|
202
|
+
"halted",
|
|
203
|
+
]);
|
|
204
|
+
const approvednodes = stepstimelinenodes({ plan, now });
|
|
205
|
+
expect(approvednodes[0]?.status).toBe("running");
|
|
206
|
+
expect(approvednodes[0]?.active).toBe(true);
|
|
207
|
+
expect(activetimelineanchor(approvednodes)).toBe("#step-s1");
|
|
208
|
+
expect(approvednodes[3]?.anchor).toBe("#step-s4");
|
|
209
|
+
const failed: planprogress = {
|
|
210
|
+
planid: plan.id,
|
|
211
|
+
completedsteps: [],
|
|
212
|
+
outcomes: [{ stepid: "s1", ok: false, summary: "The table never loaded.", at: now }],
|
|
213
|
+
updatedat: now,
|
|
214
|
+
};
|
|
215
|
+
expect(stepstimelinenodes({ plan, progress: failed, now })[0]?.status).toBe("failed");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("appends logstream events behind the hash chain with level, source and step ref", async () => {
|
|
219
|
+
const first = await logstreameventof({
|
|
220
|
+
level: "info",
|
|
221
|
+
source: "background",
|
|
222
|
+
origin: plan.origin,
|
|
223
|
+
summary: "The session started.",
|
|
224
|
+
masked: false,
|
|
225
|
+
maskverdict: "The source payload carries no masked value.",
|
|
226
|
+
previous: logstreamgenesis,
|
|
227
|
+
at: now,
|
|
228
|
+
});
|
|
229
|
+
expect(first.hash.previous).toBe(logstreamgenesis);
|
|
230
|
+
expect(first.level).toBe("info");
|
|
231
|
+
const second = await logstreameventof({
|
|
232
|
+
level: "warn",
|
|
233
|
+
source: "background",
|
|
234
|
+
origin: plan.origin,
|
|
235
|
+
summary: "The step waited at its gate.",
|
|
236
|
+
stepid: "s3",
|
|
237
|
+
masked: true,
|
|
238
|
+
maskverdict: "The card value stayed masked.",
|
|
239
|
+
previous: first.hash.current,
|
|
240
|
+
at: now + 10,
|
|
241
|
+
});
|
|
242
|
+
expect(second.hash.previous).toBe(first.hash.current);
|
|
243
|
+
const events = appendlogstreamevent(appendlogstreamevent([], first), second);
|
|
244
|
+
expect(events).toHaveLength(2);
|
|
245
|
+
await expect(
|
|
246
|
+
logstreameventof({
|
|
247
|
+
level: "info",
|
|
248
|
+
source: "background",
|
|
249
|
+
origin: plan.origin,
|
|
250
|
+
summary: " ",
|
|
251
|
+
masked: false,
|
|
252
|
+
maskverdict: "The source payload carries no masked value.",
|
|
253
|
+
previous: logstreamgenesis,
|
|
254
|
+
at: now,
|
|
255
|
+
}),
|
|
256
|
+
).rejects.toThrow(/summary/);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("verifies the logstream hash chain live and names the broken link", async () => {
|
|
260
|
+
const first = await logstreameventof({
|
|
261
|
+
level: "info",
|
|
262
|
+
source: "background",
|
|
263
|
+
origin: plan.origin,
|
|
264
|
+
summary: "First event.",
|
|
265
|
+
masked: false,
|
|
266
|
+
maskverdict: "The source payload carries no masked value.",
|
|
267
|
+
previous: logstreamgenesis,
|
|
268
|
+
at: now,
|
|
269
|
+
});
|
|
270
|
+
const second = await logstreameventof({
|
|
271
|
+
level: "error",
|
|
272
|
+
source: "background",
|
|
273
|
+
origin: plan.origin,
|
|
274
|
+
summary: "Second event.",
|
|
275
|
+
masked: false,
|
|
276
|
+
maskverdict: "The source payload carries no masked value.",
|
|
277
|
+
previous: first.hash.current,
|
|
278
|
+
at: now + 1,
|
|
279
|
+
});
|
|
280
|
+
expect((await verifylogstream([first, second])).valid).toBe(true);
|
|
281
|
+
const tampered = { ...second, summary: "Tampered summary." };
|
|
282
|
+
const broken = await verifylogstream([first, tampered]);
|
|
283
|
+
expect(broken.valid).toBe(false);
|
|
284
|
+
expect(broken.brokenat).toBe(1);
|
|
285
|
+
expect(broken.reason).toMatch(/hash does not reproduce/);
|
|
286
|
+
const relinked = { ...second, hash: { ...second.hash, previous: logstreamgenesis } };
|
|
287
|
+
expect((await verifylogstream([first, relinked])).reason).toMatch(/does not link/);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("filters the logstream by level, origin and step ref and keeps the bounded live window only", async () => {
|
|
291
|
+
const first = await logstreameventof({
|
|
292
|
+
level: "info",
|
|
293
|
+
source: "background",
|
|
294
|
+
origin: "https://a.example",
|
|
295
|
+
summary: "Info of a.",
|
|
296
|
+
masked: false,
|
|
297
|
+
maskverdict: "The source payload carries no masked value.",
|
|
298
|
+
previous: logstreamgenesis,
|
|
299
|
+
at: now,
|
|
300
|
+
});
|
|
301
|
+
const second = await logstreameventof({
|
|
302
|
+
level: "warn",
|
|
303
|
+
source: "background",
|
|
304
|
+
origin: "https://b.example",
|
|
305
|
+
summary: "Warn of b.",
|
|
306
|
+
stepid: "s3",
|
|
307
|
+
masked: false,
|
|
308
|
+
maskverdict: "The source payload carries no masked value.",
|
|
309
|
+
previous: first.hash.current,
|
|
310
|
+
at: now + 1,
|
|
311
|
+
});
|
|
312
|
+
const third = await logstreameventof({
|
|
313
|
+
level: "warn",
|
|
314
|
+
source: "background",
|
|
315
|
+
origin: "https://a.example",
|
|
316
|
+
summary: "Warn of a.",
|
|
317
|
+
masked: false,
|
|
318
|
+
maskverdict: "The source payload carries no masked value.",
|
|
319
|
+
previous: second.hash.current,
|
|
320
|
+
at: now + 2,
|
|
321
|
+
});
|
|
322
|
+
const events = [first, second, third];
|
|
323
|
+
expect(filterlogstream(events, {})).toHaveLength(3);
|
|
324
|
+
expect(filterlogstream(events, { level: "warn" })).toHaveLength(2);
|
|
325
|
+
expect(filterlogstream(events, { origin: "https://a.example" })).toHaveLength(2);
|
|
326
|
+
expect(filterlogstream(events, { stepid: "s3" })).toHaveLength(1);
|
|
327
|
+
expect(filterlogstream(events, { level: "warn", origin: "https://a.example" })).toHaveLength(1);
|
|
328
|
+
expect(livebufferof(events, undefined)).toHaveLength(3);
|
|
329
|
+
expect(livebufferof(events, 2)).toEqual([second, third]);
|
|
330
|
+
expect(livebufferof(events, 1)).toEqual([third]);
|
|
331
|
+
expect(livebufferof(events, 0)).toHaveLength(3);
|
|
332
|
+
expect(logbufferboundvalid(undefined).allowed).toBe(true);
|
|
333
|
+
expect(logbufferboundvalid(10).reason).toMatch(/user configured choice/);
|
|
334
|
+
expect(logbufferboundvalid(0).allowed).toBe(false);
|
|
335
|
+
expect(logbufferboundvalid(2.5).allowed).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("copies a verified range as an audit excerpt and refuses an unverified chain", async () => {
|
|
339
|
+
const first = await logstreameventof({
|
|
340
|
+
level: "info",
|
|
341
|
+
source: "background",
|
|
342
|
+
origin: plan.origin,
|
|
343
|
+
summary: "First event.",
|
|
344
|
+
masked: false,
|
|
345
|
+
maskverdict: "The source payload carries no masked value.",
|
|
346
|
+
previous: logstreamgenesis,
|
|
347
|
+
at: now,
|
|
348
|
+
});
|
|
349
|
+
const second = await logstreameventof({
|
|
350
|
+
level: "warn",
|
|
351
|
+
source: "background",
|
|
352
|
+
origin: plan.origin,
|
|
353
|
+
summary: "Second event.",
|
|
354
|
+
stepid: "s1",
|
|
355
|
+
masked: true,
|
|
356
|
+
maskverdict: "The typed value stayed masked.",
|
|
357
|
+
previous: first.hash.current,
|
|
358
|
+
at: now + 1,
|
|
359
|
+
});
|
|
360
|
+
const excerpt = await auditexcerptof([first, second], { from: 0, to: 2 });
|
|
361
|
+
expect(excerpt.ok).toBe(true);
|
|
362
|
+
expect(excerpt.text).toContain("First event.");
|
|
363
|
+
expect(excerpt.text).toContain("step s1");
|
|
364
|
+
expect(excerpt.text).toContain("The typed value stayed masked.");
|
|
365
|
+
const gate = logstreamegressgate({ verified: excerpt.ok, entries: 2 });
|
|
366
|
+
expect(gate.allowed).toBe(true);
|
|
367
|
+
expect(logstreamegressgate({ verified: false, entries: 2 }).reason).toMatch(/refuses the copy/);
|
|
368
|
+
expect(logstreamegressgate({ verified: true, entries: 0 }).allowed).toBe(false);
|
|
369
|
+
const tampered = { ...second, summary: "Tampered." };
|
|
370
|
+
const refused = await auditexcerptof([first, tampered], { from: 0, to: 2 });
|
|
371
|
+
expect(refused.ok).toBe(false);
|
|
372
|
+
expect(refused.reason).toMatch(/refuses the copy/);
|
|
373
|
+
expect((await auditexcerptof([first, second], { from: 5, to: 9 })).ok).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("maps audit kinds onto the logstream levels their events carry", () => {
|
|
377
|
+
expect(loglevelof("error")).toBe("error");
|
|
378
|
+
expect(loglevelof("deny")).toBe("warn");
|
|
379
|
+
expect(loglevelof("revoke")).toBe("warn");
|
|
380
|
+
expect(loglevelof("approval")).toBe("info");
|
|
381
|
+
expect(loglevelof("action")).toBe("info");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { matrixtargetof, matrixverify, platformmatrixof, platformtargets, targetoutput } from "../runtime.js";
|
|
3
|
+
import { platformmatrixgate } from "../policy.js";
|
|
4
|
+
import type { platformtarget } from "../types.js";
|
|
5
|
+
|
|
6
|
+
describe("the platform matrix", () => {
|
|
7
|
+
it("declares the browser, node, bun and deno targets in one matrix", () => {
|
|
8
|
+
const targets = platformtargets();
|
|
9
|
+
expect(targets.map((target) => target.runtime)).toEqual(["browser", "node", "bun", "deno"]);
|
|
10
|
+
expect(targets.every((target) => target.declarations)).toBe(true);
|
|
11
|
+
expect(targets.every((target) => target.entry.endsWith(".ts"))).toBe(true);
|
|
12
|
+
expect(matrixtargetof(targets, "browser")).toMatchObject({ entry: "umd.ts", format: "umd", platform: "browser" });
|
|
13
|
+
expect(matrixtargetof(targets, "node")).toMatchObject({ entry: "node.ts", format: "cjs", platform: "node" });
|
|
14
|
+
expect(matrixtargetof(targets, "bun")).toMatchObject({ entry: "bun.ts", format: "esm", platform: "node" });
|
|
15
|
+
expect(matrixtargetof(targets, "deno")).toMatchObject({ entry: "deno.ts", format: "esm", platform: "neutral" });
|
|
16
|
+
expect(() => matrixtargetof(targets, "browser" as never)).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("gates the matrix so every runtime declares exactly one target with its declaration set", () => {
|
|
20
|
+
const targets = platformtargets();
|
|
21
|
+
expect(platformmatrixgate(targets).allowed).toBe(true);
|
|
22
|
+
expect(platformmatrixgate(targets.slice(0, 3)).allowed).toBe(false);
|
|
23
|
+
expect(platformmatrixgate([...targets, targets[0] as platformtarget]).allowed).toBe(false);
|
|
24
|
+
expect(platformmatrixgate(targets.map((target) => ({ ...target, declarations: false }))).allowed).toBe(false);
|
|
25
|
+
expect(platformmatrixgate(targets.map((target) => ({ ...target, entry: " " }))).allowed).toBe(false);
|
|
26
|
+
const umdonnode = targets.map((target) =>
|
|
27
|
+
target.runtime === "node" ? { ...target, format: "umd" as const } : target,
|
|
28
|
+
);
|
|
29
|
+
expect(platformmatrixgate(umdonnode).allowed).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("completes the matrix only when every runtime declares exactly one target", () => {
|
|
33
|
+
expect(platformmatrixof(platformtargets()).complete).toBe(true);
|
|
34
|
+
expect(platformmatrixof(platformtargets().slice(1)).complete).toBe(false);
|
|
35
|
+
expect(platformmatrixof([...platformtargets(), platformtargets()[0] as platformtarget]).complete).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("verifies one matrix run against the built entries it finds", () => {
|
|
39
|
+
const targets = platformtargets();
|
|
40
|
+
expect(matrixverify({ targets, present: ["umd.ts", "node.ts", "bun.ts", "deno.ts"] })).toMatchObject({
|
|
41
|
+
complete: true,
|
|
42
|
+
missing: [],
|
|
43
|
+
});
|
|
44
|
+
const missing = matrixverify({ targets, present: ["umd.ts", "node.ts"] });
|
|
45
|
+
expect(missing.complete).toBe(false);
|
|
46
|
+
expect(missing.missing).toEqual(["bun:bun.ts", "deno:deno.ts"]);
|
|
47
|
+
expect(missing.reason).toMatch(/lacks the built entries/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reads the output file name of every target bundle", () => {
|
|
51
|
+
expect(targetoutput(matrixtargetof(platformtargets(), "browser"))).toBe("devthink.umd.js");
|
|
52
|
+
expect(targetoutput(matrixtargetof(platformtargets(), "node"))).toBe("node.cjs");
|
|
53
|
+
expect(targetoutput(matrixtargetof(platformtargets(), "bun"))).toBe("bun.js");
|
|
54
|
+
expect(targetoutput(matrixtargetof(platformtargets(), "deno"))).toBe("deno.js");
|
|
55
|
+
});
|
|
56
|
+
});
|