@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,405 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
allowlistcheck,
|
|
4
|
+
classconsentcovers,
|
|
5
|
+
consentprompttext,
|
|
6
|
+
consentwindowstate,
|
|
7
|
+
deniedevidenceof,
|
|
8
|
+
exactorigin,
|
|
9
|
+
expireconsentwindows,
|
|
10
|
+
haltedstepsof,
|
|
11
|
+
missingclassconsents,
|
|
12
|
+
openconsentwindow,
|
|
13
|
+
originprofileof,
|
|
14
|
+
profilegrade,
|
|
15
|
+
profilekind,
|
|
16
|
+
profilesummary,
|
|
17
|
+
renewconsentwindow,
|
|
18
|
+
revokerun,
|
|
19
|
+
scopegrantof,
|
|
20
|
+
sensitiveclassesof,
|
|
21
|
+
wildcardentry,
|
|
22
|
+
windowgatesstep,
|
|
23
|
+
denydefaultnotice,
|
|
24
|
+
} from "../security.js";
|
|
25
|
+
import {
|
|
26
|
+
automationallowlistgate,
|
|
27
|
+
consentdurationvalid,
|
|
28
|
+
consentwindowgate,
|
|
29
|
+
logreadgate,
|
|
30
|
+
originprofilegate,
|
|
31
|
+
revokerungate,
|
|
32
|
+
sensitiveclassgate,
|
|
33
|
+
sensitivepipelingate,
|
|
34
|
+
} from "../policy.js";
|
|
35
|
+
import type { actionkind, automationallowlistentry, classconsent, originprofile, toolstep } from "../types.js";
|
|
36
|
+
|
|
37
|
+
const now = 1_000;
|
|
38
|
+
const origin = "https://example.com";
|
|
39
|
+
const other = "https://other.example";
|
|
40
|
+
|
|
41
|
+
function step(
|
|
42
|
+
kind: actionkind,
|
|
43
|
+
overrides: Partial<Pick<toolstep, "value" | "options" | "target">> = {},
|
|
44
|
+
): Pick<toolstep, "kind" | "value" | "options"> {
|
|
45
|
+
return { kind, ...overrides };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function allowlist(origins: string[]): automationallowlistentry[] {
|
|
49
|
+
return origins.map((entry) => ({ origin: entry, profileid: "default", grantedat: now }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("denydefault allowlist posture", () => {
|
|
53
|
+
it("refuses every ungranted origin under the denydefault posture", () => {
|
|
54
|
+
const verdict = allowlistcheck({
|
|
55
|
+
origin,
|
|
56
|
+
allowlist: allowlist([other]),
|
|
57
|
+
profileid: "default",
|
|
58
|
+
sessionorigin: other,
|
|
59
|
+
});
|
|
60
|
+
expect(verdict.allowed).toBe(false);
|
|
61
|
+
expect(verdict.reason).toMatch(/denydefault posture refuses https:\/\/example\.com/i);
|
|
62
|
+
const gate = automationallowlistgate({
|
|
63
|
+
origin,
|
|
64
|
+
allowlist: allowlist([other]),
|
|
65
|
+
session: { id: "s1", tabid: 1, origin: other, startedat: now, expiresat: now + 1000 },
|
|
66
|
+
});
|
|
67
|
+
expect(gate.allowed).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("grants a listed origin and treats the active tab as one explicit single origin grant", () => {
|
|
71
|
+
expect(allowlistcheck({ origin, allowlist: allowlist([origin]), profileid: "default" }).allowed).toBe(true);
|
|
72
|
+
const tabgrant = allowlistcheck({ origin, allowlist: [], profileid: "default", sessionorigin: origin });
|
|
73
|
+
expect(tabgrant.allowed).toBe(true);
|
|
74
|
+
expect(tabgrant.reason).toMatch(/exactly one explicit single origin grant/i);
|
|
75
|
+
const differenttab = allowlistcheck({ origin, allowlist: [], profileid: "default", sessionorigin: other });
|
|
76
|
+
expect(differenttab.allowed).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("matches origins exactly with no wildcard expansion and refuses wildcard entries", () => {
|
|
80
|
+
expect(exactorigin(origin, origin)).toBe(true);
|
|
81
|
+
expect(exactorigin(origin, "https://*.example.com")).toBe(false);
|
|
82
|
+
expect(exactorigin("https://a.example.com", "https://example.com")).toBe(false);
|
|
83
|
+
const wildcard = allowlistcheck({ origin, allowlist: allowlist(["https://*.example.com"]), profileid: "default" });
|
|
84
|
+
expect(wildcard.allowed).toBe(false);
|
|
85
|
+
expect(wildcard.reason).toMatch(/wildcard/i);
|
|
86
|
+
expect(wildcardentry("https://*/*")).toBe(true);
|
|
87
|
+
expect(wildcardentry("*")).toBe(true);
|
|
88
|
+
expect(wildcardentry(origin)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("scopes the allowlist per profile workspace", () => {
|
|
92
|
+
const scoped = allowlistcheck({
|
|
93
|
+
origin,
|
|
94
|
+
allowlist: [{ origin, profileid: "otherprofile", grantedat: now }],
|
|
95
|
+
profileid: "default",
|
|
96
|
+
});
|
|
97
|
+
expect(scoped.allowed).toBe(false);
|
|
98
|
+
expect(scoped.reason).toMatch(/absent from the automation allowlist/i);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("origin profiles per site", () => {
|
|
103
|
+
it("builds a profile and moves a kind between its grants and denials", () => {
|
|
104
|
+
const profile = originprofileof({ origin, grants: ["submitform" as actionkind], now });
|
|
105
|
+
expect(profile.grants).toEqual(["submitform"]);
|
|
106
|
+
const denied = profilekind({ profile, kind: "submitform", decision: "deny", now: now + 1 });
|
|
107
|
+
expect(denied.denials).toEqual(["submitform"]);
|
|
108
|
+
expect(denied.grants).toEqual([]);
|
|
109
|
+
const granted = profilekind({ profile: denied, kind: "fillcard", decision: "grant", now: now + 2 });
|
|
110
|
+
expect(granted.grants).toEqual(["fillcard"]);
|
|
111
|
+
expect(granted.denials).toEqual(["submitform"]);
|
|
112
|
+
expect(() => originprofileof({ origin: " ", now })).toThrow(/exact origin/i);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("consults the profile before sensitive kinds and refuses denied kinds", () => {
|
|
116
|
+
const profile: originprofile = {
|
|
117
|
+
profileid: "p1",
|
|
118
|
+
origin,
|
|
119
|
+
grants: ["fillcard" as actionkind],
|
|
120
|
+
denials: ["submitform" as actionkind],
|
|
121
|
+
createdat: now,
|
|
122
|
+
updatedat: now,
|
|
123
|
+
};
|
|
124
|
+
expect(profilegrade({ profile, kind: "submitform", sensitive: true }).allowed).toBe(false);
|
|
125
|
+
expect(profilegrade({ profile, kind: "submitform", sensitive: true }).reason).toMatch(
|
|
126
|
+
/denies the submitform kind/i,
|
|
127
|
+
);
|
|
128
|
+
expect(profilegrade({ profile, kind: "fillcard", sensitive: true }).allowed).toBe(true);
|
|
129
|
+
expect(originprofilegate({ profile, kind: "submitform", sensitive: true }).allowed).toBe(false);
|
|
130
|
+
expect(originprofilegate({ profile: undefined, kind: "submitform", sensitive: true }).allowed).toBe(true);
|
|
131
|
+
expect(profilegrade({ profile, kind: "readtext", sensitive: false }).consult).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("summarizes the profile of the active tab for the popup", () => {
|
|
135
|
+
expect(profilesummary(undefined)).toMatch(/no origin profile/i);
|
|
136
|
+
expect(
|
|
137
|
+
profilesummary({
|
|
138
|
+
profileid: "p1",
|
|
139
|
+
origin,
|
|
140
|
+
grants: ["fillcard" as actionkind],
|
|
141
|
+
denials: ["submitform" as actionkind],
|
|
142
|
+
createdat: now,
|
|
143
|
+
updatedat: now,
|
|
144
|
+
}),
|
|
145
|
+
).toMatch(/grants 1 kind and denies 1 kind/i);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("sensitive classes and fresh consents", () => {
|
|
150
|
+
it("classifies sensitive kinds into payment, credential, delete and publish classes", () => {
|
|
151
|
+
expect(sensitiveclassesof(step("fillcard")).classes).toEqual(["payment"]);
|
|
152
|
+
expect(sensitiveclassesof(step("consentpassword")).classes).toEqual(["credential"]);
|
|
153
|
+
expect(sensitiveclassesof(step("discardtab")).classes).toEqual(["delete"]);
|
|
154
|
+
expect(sensitiveclassesof(step("postfiles")).classes).toEqual(["publish"]);
|
|
155
|
+
expect(sensitiveclassesof(step("readtext")).sensitive).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("refines the classification by kind options instead of kind names alone", () => {
|
|
159
|
+
const cardform = step("fillform", { options: JSON.stringify({ fields: [{ name: "cardnumber", value: "4242" }] }) });
|
|
160
|
+
expect(sensitiveclassesof(cardform).classes).toContain("payment");
|
|
161
|
+
const credentialsubmit = step("submitform", {
|
|
162
|
+
options: JSON.stringify({ fields: [{ name: "password", value: "hunter2" }] }),
|
|
163
|
+
});
|
|
164
|
+
expect(sensitiveclassesof(credentialsubmit).classes).toContain("credential");
|
|
165
|
+
const mutatingcall = step("callrest", { options: JSON.stringify({ method: "POST" }) });
|
|
166
|
+
expect(sensitiveclassesof(mutatingcall).classes).toContain("publish");
|
|
167
|
+
const readonlycall = sensitiveclassesof(step("callrest", { options: JSON.stringify({ method: "GET" }) }));
|
|
168
|
+
expect(readonlycall.classes).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("classifies upload, download and evaluate as sensitive by default", () => {
|
|
172
|
+
for (const kind of ["attachfile", "downloadfile", "evaluate"] as actionkind[]) {
|
|
173
|
+
const verdict = sensitiveclassesof(step(kind));
|
|
174
|
+
expect(verdict.sensitive).toBe(true);
|
|
175
|
+
expect(verdict.bydefault).toBe(true);
|
|
176
|
+
expect(verdict.classes).toEqual([]);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("classifies credential bearing form submits as the credential class", () => {
|
|
181
|
+
const verdict = sensitiveclassesof(
|
|
182
|
+
step("submitform", { options: JSON.stringify({ fields: [{ name: "apitoken", value: "tok" }] }) }),
|
|
183
|
+
);
|
|
184
|
+
expect(verdict.classes).toContain("credential");
|
|
185
|
+
expect(verdict.reason).toMatch(/credential/i);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("requires one fresh consent prompt per class per origin", () => {
|
|
189
|
+
const consents: classconsent[] = [{ id: "c1", origin, sensitiveclass: "payment", grantedat: now }];
|
|
190
|
+
expect(classconsentcovers(consents, origin, "payment", now + 1)).toBe(true);
|
|
191
|
+
expect(classconsentcovers(consents, origin, "credential", now + 1)).toBe(false);
|
|
192
|
+
expect(classconsentcovers(consents, other, "payment", now + 1)).toBe(false);
|
|
193
|
+
const expired: classconsent[] = [
|
|
194
|
+
{ id: "c2", origin, sensitiveclass: "payment", grantedat: now, expiresat: now + 10 },
|
|
195
|
+
];
|
|
196
|
+
expect(classconsentcovers(expired, origin, "payment", now + 10)).toBe(false);
|
|
197
|
+
const missing = missingclassconsents({
|
|
198
|
+
origin,
|
|
199
|
+
classes: ["payment", "credential"],
|
|
200
|
+
bydefault: false,
|
|
201
|
+
consents,
|
|
202
|
+
now: now + 1,
|
|
203
|
+
});
|
|
204
|
+
expect(missing.needed).toBe(true);
|
|
205
|
+
expect(missing.missing).toEqual(["credential"]);
|
|
206
|
+
expect(missingclassconsents({ origin, classes: [], bydefault: true, consents, now: now + 1 }).needed).toBe(true);
|
|
207
|
+
expect(
|
|
208
|
+
missingclassconsents({ origin, classes: ["payment"], bydefault: false, consents, now: now + 1 }).needed,
|
|
209
|
+
).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("routes sensitive steps through the consent gate and names the class in the prompt", () => {
|
|
213
|
+
const gate = sensitiveclassgate({
|
|
214
|
+
origin,
|
|
215
|
+
classes: ["payment"],
|
|
216
|
+
bydefault: false,
|
|
217
|
+
sensitive: true,
|
|
218
|
+
consents: [],
|
|
219
|
+
now,
|
|
220
|
+
});
|
|
221
|
+
expect(gate.allowed).toBe(false);
|
|
222
|
+
expect(gate.reason).toMatch(/fresh consent prompt/i);
|
|
223
|
+
const granted = sensitiveclassgate({
|
|
224
|
+
origin,
|
|
225
|
+
classes: ["payment"],
|
|
226
|
+
bydefault: false,
|
|
227
|
+
sensitive: true,
|
|
228
|
+
consents: [{ id: "c1", origin, sensitiveclass: "payment", grantedat: now }],
|
|
229
|
+
now,
|
|
230
|
+
});
|
|
231
|
+
expect(granted.allowed).toBe(true);
|
|
232
|
+
const pipeline = sensitivepipelingate({ step: step("fillcard"), profile: undefined, consents: [], origin, now });
|
|
233
|
+
expect(pipeline.allowed).toBe(false);
|
|
234
|
+
expect(pipeline.reason).toMatch(/payment/i);
|
|
235
|
+
const prompt = consentprompttext({
|
|
236
|
+
origin,
|
|
237
|
+
kind: "fillcard",
|
|
238
|
+
classes: ["payment"],
|
|
239
|
+
bydefault: false,
|
|
240
|
+
duration: 60_000,
|
|
241
|
+
});
|
|
242
|
+
expect(prompt).toMatch(/fillcard step on https:\/\/example\.com/i);
|
|
243
|
+
expect(prompt).toMatch(/payment class/i);
|
|
244
|
+
expect(prompt).toMatch(/60000 milliseconds/i);
|
|
245
|
+
expect(prompt).toMatch(/never defaults to unlimited|no grant ever defaults to unlimited/i);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("consent windows bound in time", () => {
|
|
250
|
+
it("opens a window with the user duration and a named boundary that never defaults to unlimited", () => {
|
|
251
|
+
const window = openconsentwindow({
|
|
252
|
+
sessionid: "s1",
|
|
253
|
+
origin,
|
|
254
|
+
duration: 60_000,
|
|
255
|
+
kinds: ["submitform" as actionkind],
|
|
256
|
+
now,
|
|
257
|
+
});
|
|
258
|
+
expect(window.expiresat).toBe(now + 60_000);
|
|
259
|
+
expect(window.boundary).toMatch(/60000 milliseconds the user chose/i);
|
|
260
|
+
expect(() => openconsentwindow({ sessionid: "s1", origin, duration: 0, kinds: [], now })).toThrow(
|
|
261
|
+
/positive user value/i,
|
|
262
|
+
);
|
|
263
|
+
expect(() => openconsentwindow({ sessionid: " ", origin, duration: 60_000, kinds: [], now })).toThrow(
|
|
264
|
+
/session and its exact origin/i,
|
|
265
|
+
);
|
|
266
|
+
const durationgate = consentdurationvalid(60_000);
|
|
267
|
+
expect(durationgate.allowed).toBe(true);
|
|
268
|
+
expect(consentdurationvalid(0).allowed).toBe(false);
|
|
269
|
+
expect(consentdurationvalid(0).reason).toMatch(/defaults to unlimited/i);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("scopes the window to one session and one origin and expires it at its boundary", () => {
|
|
273
|
+
const window = openconsentwindow({ sessionid: "s1", origin, duration: 60_000, kinds: [], now });
|
|
274
|
+
expect(windowgatesstep({ window, sessionid: "s1", origin, now: now + 1 }).allowed).toBe(true);
|
|
275
|
+
const otherorigin = windowgatesstep({ window, sessionid: "s1", origin: other, now: now + 1 });
|
|
276
|
+
expect(otherorigin.allowed).toBe(false);
|
|
277
|
+
expect(otherorigin.reason).toMatch(/never widens to another origin/i);
|
|
278
|
+
const othersession = windowgatesstep({ window, sessionid: "s2", origin, now: now + 1 });
|
|
279
|
+
expect(othersession.reason).toMatch(/never widens to another session/i);
|
|
280
|
+
const expired = windowgatesstep({ window, sessionid: "s1", origin, now: now + 60_000 });
|
|
281
|
+
expect(expired.allowed).toBe(false);
|
|
282
|
+
expect(expired.suspended).toBe(true);
|
|
283
|
+
expect(expired.reason).toMatch(/run suspends/i);
|
|
284
|
+
expect(consentwindowstate(window, now + 59_999).remaining).toBe(1);
|
|
285
|
+
expect(consentwindowgate({ window, sessionid: "s1", origin, sensitive: true, now: now + 60_000 }).allowed).toBe(
|
|
286
|
+
false,
|
|
287
|
+
);
|
|
288
|
+
expect(consentwindowgate({ window: undefined, sessionid: "s1", origin, sensitive: true, now }).allowed).toBe(false);
|
|
289
|
+
expect(consentwindowgate({ window, sessionid: "s1", origin, sensitive: false, now: now + 60_000 }).allowed).toBe(
|
|
290
|
+
true,
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("expires closed windows past their boundary and renews only through a new explicit prompt", () => {
|
|
295
|
+
const windows = [
|
|
296
|
+
openconsentwindow({ sessionid: "s1", origin, duration: 10, kinds: [], now }),
|
|
297
|
+
openconsentwindow({ sessionid: "s1", origin: other, duration: 10_000, kinds: [], now }),
|
|
298
|
+
];
|
|
299
|
+
const expired = expireconsentwindows(windows, now + 11);
|
|
300
|
+
expect(expired[0]?.state).toBe("closed");
|
|
301
|
+
expect(expired[0]?.closedat).toBe(now + 11);
|
|
302
|
+
expect(expired[1]?.state).toBe("active");
|
|
303
|
+
const { renewed, closed } = renewconsentwindow({
|
|
304
|
+
window: expired[0] as (typeof windows)[0],
|
|
305
|
+
duration: 30_000,
|
|
306
|
+
kinds: [],
|
|
307
|
+
now: now + 12,
|
|
308
|
+
});
|
|
309
|
+
expect(renewed.state).toBe("active");
|
|
310
|
+
expect(renewed.expiresat).toBe(now + 12 + 30_000);
|
|
311
|
+
expect(closed.state).toBe("closed");
|
|
312
|
+
expect(renewed.id).not.toBe(closed.id);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("revokerun halts the run mid step", () => {
|
|
317
|
+
it("revokes the run as a terminal session event halting the pending and queued steps", () => {
|
|
318
|
+
const revocation = revokerun({
|
|
319
|
+
sessionid: "s1",
|
|
320
|
+
runid: "run1",
|
|
321
|
+
pendingstepid: "s3",
|
|
322
|
+
queuedstepids: ["s4", "s5"],
|
|
323
|
+
actor: "user",
|
|
324
|
+
now,
|
|
325
|
+
});
|
|
326
|
+
expect(revocation.haltedstepids).toEqual(["s3", "s4", "s5"]);
|
|
327
|
+
expect(haltedstepsof(revocation)).toEqual({ pending: "s3", queued: ["s4", "s5"] });
|
|
328
|
+
const gate = revokerungate({ revocation, sessionid: "s1", runid: "run1" });
|
|
329
|
+
expect(gate.allowed).toBe(false);
|
|
330
|
+
expect(gate.reason).toMatch(/halted/i);
|
|
331
|
+
expect(revokerungate({ revocation, sessionid: "s1", runid: "run2" }).allowed).toBe(true);
|
|
332
|
+
expect(revokerungate({ revocation: undefined, sessionid: "s1", runid: "run1" }).allowed).toBe(true);
|
|
333
|
+
expect(() => revokerun({ sessionid: "s1", runid: "run1", actor: "user", now })).toThrow(
|
|
334
|
+
/at least the pending step/i,
|
|
335
|
+
);
|
|
336
|
+
expect(() => revokerun({ sessionid: "s1", runid: "run1", pendingstepid: "s3", actor: " ", now })).toThrow(
|
|
337
|
+
/acting user/i,
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("writes the consent scope grant and the denied evidence records", () => {
|
|
342
|
+
const scope = scopegrantof({ origin, kinds: ["observe" as actionkind], boundary: "the session expiry", now });
|
|
343
|
+
expect(scope).toEqual({ origin, kinds: ["observe"], boundary: "the session expiry", grantedat: now });
|
|
344
|
+
expect(() => scopegrantof({ origin, kinds: [], boundary: "b", now })).toThrow(/names the kinds/i);
|
|
345
|
+
expect(() => scopegrantof({ origin, kinds: ["observe" as actionkind], boundary: " ", now })).toThrow(/boundary/i);
|
|
346
|
+
const denied = deniedevidenceof({
|
|
347
|
+
origin,
|
|
348
|
+
kind: "submitform",
|
|
349
|
+
reason: "The denydefault posture refuses the origin.",
|
|
350
|
+
now,
|
|
351
|
+
});
|
|
352
|
+
expect(denied).toEqual({
|
|
353
|
+
origin,
|
|
354
|
+
kind: "submitform",
|
|
355
|
+
reason: "The denydefault posture refuses the origin.",
|
|
356
|
+
at: now,
|
|
357
|
+
});
|
|
358
|
+
expect(denydefaultnotice(other)).toMatch(/denydefault posture refuses https:\/\/other\.example/i);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("refuses log reads of a chain with a broken link", () => {
|
|
362
|
+
expect(logreadgate({ valid: true }).allowed).toBe(true);
|
|
363
|
+
const broken = logreadgate({ valid: false, brokenat: 2 });
|
|
364
|
+
expect(broken.allowed).toBe(false);
|
|
365
|
+
expect(broken.reason).toMatch(/entry 2/i);
|
|
366
|
+
expect(broken.reason).toMatch(/refuses the read/i);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
import { safedefaultnotice, safedefaultprofile, safedefaultreadkind } from "../security.js";
|
|
371
|
+
import { safedefaultsgate } from "../policy.js";
|
|
372
|
+
|
|
373
|
+
describe("originpolicy safedefaults", () => {
|
|
374
|
+
it("profiles an unknown origin as reads only with every sensitive class denied", () => {
|
|
375
|
+
const profile = safedefaultprofile({ origin, now });
|
|
376
|
+
expect(profile.origin).toBe(origin);
|
|
377
|
+
expect(profile.grants).toContain("observe");
|
|
378
|
+
expect(profile.grants).toContain("readhtml");
|
|
379
|
+
expect(profile.denials).toContain("fillcard");
|
|
380
|
+
expect(profile.denials).toContain("consentpassword");
|
|
381
|
+
expect(profile.denials).toContain("discardtab");
|
|
382
|
+
expect(profile.denials).toContain("postform");
|
|
383
|
+
expect(() => safedefaultprofile({ origin: " ", now })).toThrow(/exact origin/);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("recognises the documented reads only baseline kinds", () => {
|
|
387
|
+
expect(safedefaultreadkind("observe")).toBe(true);
|
|
388
|
+
expect(safedefaultreadkind("readtext")).toBe(true);
|
|
389
|
+
expect(safedefaultreadkind("click")).toBe(false);
|
|
390
|
+
expect(safedefaultreadkind("fillcard")).toBe(false);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("renders the safedefaults notice that links to the originprofile editor", () => {
|
|
394
|
+
expect(safedefaultnotice(origin)).toMatch(/reads only/);
|
|
395
|
+
expect(safedefaultnotice(origin)).toMatch(/originprofile editor/);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("denies sensitive classes under safedefaults while reads pass", () => {
|
|
399
|
+
expect(safedefaultsgate({ profile: undefined, classes: [], sensitive: false }).allowed).toBe(true);
|
|
400
|
+
expect(safedefaultsgate({ profile: undefined, classes: ["payment"], sensitive: true }).allowed).toBe(false);
|
|
401
|
+
expect(
|
|
402
|
+
safedefaultsgate({ profile: safedefaultprofile({ origin, now }), classes: ["payment"], sensitive: true }).allowed,
|
|
403
|
+
).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
comparemetricdefaults,
|
|
4
|
+
comparesessionmetrics,
|
|
5
|
+
firstdivergenceof,
|
|
6
|
+
joinruns,
|
|
7
|
+
outputcomparesessionof,
|
|
8
|
+
outputcompareview,
|
|
9
|
+
stepcomparisonof,
|
|
10
|
+
taskinputsignatureof,
|
|
11
|
+
} from "../evidence.js";
|
|
12
|
+
import { outputcomparegate, outputcomparereadonlygate } from "../policy.js";
|
|
13
|
+
import type { runlogentry } from "../types.js";
|
|
14
|
+
|
|
15
|
+
const now = 1_800_000_000_000;
|
|
16
|
+
|
|
17
|
+
function entry(
|
|
18
|
+
stepid: string,
|
|
19
|
+
state: runlogentry["state"],
|
|
20
|
+
summary: string,
|
|
21
|
+
duration: number,
|
|
22
|
+
startedat = now,
|
|
23
|
+
): runlogentry {
|
|
24
|
+
return { stepid, label: stepid, state, startedat, duration, summary };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("outputcompare of two runs", () => {
|
|
28
|
+
it("gates the comparison to runs that share a task input signature and never executes a step", () => {
|
|
29
|
+
const signaturea = taskinputsignatureof({
|
|
30
|
+
objective: "digest the invoice table",
|
|
31
|
+
steps: ["focus", "inspect", "click"],
|
|
32
|
+
});
|
|
33
|
+
const signatureb = taskinputsignatureof({
|
|
34
|
+
objective: "digest the invoice table",
|
|
35
|
+
steps: ["focus", "inspect", "click"],
|
|
36
|
+
});
|
|
37
|
+
const signaturec = taskinputsignatureof({ objective: "digest the invoice table", steps: ["focus", "inspect"] });
|
|
38
|
+
expect(signaturea).toBe(signatureb);
|
|
39
|
+
expect(signaturea).not.toBe(signaturec);
|
|
40
|
+
expect(outputcomparegate({ signaturea, signatureb }).allowed).toBe(true);
|
|
41
|
+
expect(outputcomparegate({ signaturea, signatureb: signaturec }).allowed).toBe(false);
|
|
42
|
+
expect(outputcomparegate({ signaturea, signatureb: signaturec }).reason).toMatch(/different task input signatures/);
|
|
43
|
+
expect(outputcomparegate({ signaturea: "", signatureb: "" }).allowed).toBe(false);
|
|
44
|
+
expect(outputcomparereadonlygate({ executessteps: true }).allowed).toBe(false);
|
|
45
|
+
expect(outputcomparereadonlygate({ executessteps: true }).reason).toMatch(/never executes/);
|
|
46
|
+
expect(outputcomparereadonlygate({ executessteps: false }).allowed).toBe(true);
|
|
47
|
+
expect(comparemetricdefaults()).toEqual(["agreement", "divergence", "durationdelta"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("joins two runs on their step sequence and grades agreement, divergence and duration deltas", () => {
|
|
51
|
+
const logsa = [
|
|
52
|
+
entry("s1", "done", "focused the table", 100),
|
|
53
|
+
entry("s2", "done", "clicked export", 200),
|
|
54
|
+
entry("s3", "done", "only in run a", 50),
|
|
55
|
+
];
|
|
56
|
+
const logsb = [entry("s1", "done", "focused the table", 120), entry("s2", "done", "clicked export twice", 300)];
|
|
57
|
+
const joined = joinruns(logsa, logsb);
|
|
58
|
+
expect(joined.map((pair) => pair.stepid)).toEqual(["s1", "s2", "s3"]);
|
|
59
|
+
expect(joined[2]?.a).toBeDefined();
|
|
60
|
+
expect(joined[2]?.b).toBeUndefined();
|
|
61
|
+
const grades = joined.map((pair) => stepcomparisonof(pair));
|
|
62
|
+
expect(grades[0]?.agreement).toBe("agree");
|
|
63
|
+
expect(grades[0]?.durationdelta).toBe(-20);
|
|
64
|
+
expect(grades[1]?.agreement).toBe("diverge");
|
|
65
|
+
expect(grades[1]?.durationdelta).toBe(-100);
|
|
66
|
+
expect(grades[2]?.agreement).toBe("onlyone");
|
|
67
|
+
expect(grades[2]?.durationdelta).toBe(0);
|
|
68
|
+
expect(firstdivergenceof(grades)).toBe(1);
|
|
69
|
+
expect(firstdivergenceof([grades[0] as never])).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("builds the comparison session with its metric set and highlights the first divergence", () => {
|
|
73
|
+
const logsa = [entry("s1", "done", "focused", 100), entry("s2", "done", "clicked", 200)];
|
|
74
|
+
const logsb = [entry("s1", "done", "focused", 100), entry("s2", "done", "clicked differently", 260)];
|
|
75
|
+
const session = outputcomparesessionof({ runids: ["runa", "runb"], logsa, logsb, now });
|
|
76
|
+
expect(session.runids).toEqual(["runa", "runb"]);
|
|
77
|
+
expect(session.metrics).toEqual(["agreement", "divergence", "durationdelta"]);
|
|
78
|
+
expect(session.firstdivergence).toBe(1);
|
|
79
|
+
expect(() => outputcomparesessionof({ runids: ["runa", " "], logsa, logsb, now })).toThrow(/both run ids/);
|
|
80
|
+
expect(() => outputcomparesessionof({ runids: ["runa", "runa"], logsa, logsb, now })).toThrow(/distinct runs/);
|
|
81
|
+
const view = outputcompareview(session);
|
|
82
|
+
expect(view[1]?.highlighted).toBe(true);
|
|
83
|
+
expect(view[0]?.highlighted).toBe(false);
|
|
84
|
+
const metrics = comparesessionmetrics(session);
|
|
85
|
+
expect(metrics.agree).toBe(1);
|
|
86
|
+
expect(metrics.diverge).toBe(1);
|
|
87
|
+
expect(metrics.reason).toMatch(/first divergence at step index 1/);
|
|
88
|
+
const agreeing = outputcomparesessionof({
|
|
89
|
+
runids: ["runa", "runb"],
|
|
90
|
+
logsa: [entry("s1", "done", "same", 10)],
|
|
91
|
+
logsb: [entry("s1", "done", "same", 10)],
|
|
92
|
+
now,
|
|
93
|
+
});
|
|
94
|
+
expect(agreeing.firstdivergence).toBeUndefined();
|
|
95
|
+
expect(comparesessionmetrics(agreeing).reason).toMatch(/agreement across the whole sequence/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
/* The zero-loss governance gate of the grand merge: the three lineages shipped
|
|
7
|
+
* contribution guides, a code of conduct, a code owners roster and the library
|
|
8
|
+
* surfaces of the gateway and maene families (the gateway README documented the
|
|
9
|
+
* npm library, the maene root README documented its public api). This gate
|
|
10
|
+
* keeps the restored artifacts present, keeps the exports subpaths pointing at
|
|
11
|
+
* real repository files and keeps the docs naming the surfaces they document,
|
|
12
|
+
* so a regression that drops a lineage surface goes red before the release. */
|
|
13
|
+
|
|
14
|
+
describe("package governance surface", () => {
|
|
15
|
+
it("carries the merged contributing guide, the code of conduct and the code owners of the lineage rosters", async () => {
|
|
16
|
+
const contributing = await readFile("CONTRIBUTING.md", "utf8");
|
|
17
|
+
expect(contributing).toContain("Contributing to DevThink");
|
|
18
|
+
/* the gateway lineage contract */
|
|
19
|
+
expect(contributing).toContain("bun run check");
|
|
20
|
+
expect(contributing).toContain("bun run db:generate");
|
|
21
|
+
expect(contributing).toContain("conventional commits");
|
|
22
|
+
/* the extension lineage contracts */
|
|
23
|
+
expect(contributing).toContain("Clean-room policy");
|
|
24
|
+
expect(contributing).toContain("library-first and dependency-light");
|
|
25
|
+
expect(contributing).toContain("Adding a cli command");
|
|
26
|
+
expect(contributing).toContain("Adding a build target");
|
|
27
|
+
expect(contributing).toContain("Adding a tool to the catalog");
|
|
28
|
+
expect(contributing).toContain("Adding a native surface");
|
|
29
|
+
expect(contributing).toContain("Adding a release channel");
|
|
30
|
+
expect(contributing).toContain("docs/kinddocs.md");
|
|
31
|
+
expect(contributing).toContain("node tests/doccheck.mjs");
|
|
32
|
+
/* the maene lineage contract and the merged release lockstep */
|
|
33
|
+
expect(contributing).toContain("tests/provider");
|
|
34
|
+
expect(contributing).toContain("sync:metadata");
|
|
35
|
+
expect(contributing).toContain("GPL-3.0-only");
|
|
36
|
+
|
|
37
|
+
const conduct = await readFile("code-of-conduct.md", "utf8");
|
|
38
|
+
expect(conduct).toContain("DevThink code of conduct");
|
|
39
|
+
expect(conduct).toContain("wenathlan and contributors");
|
|
40
|
+
expect(conduct).toContain("SECURITY.md");
|
|
41
|
+
|
|
42
|
+
const owners = await readFile("CODEOWNERS", "utf8");
|
|
43
|
+
expect(owners).toMatch(/^\* @iakadion @inathlan @aasblor @nasblor$/m);
|
|
44
|
+
expect(owners).toContain("devthink");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("routes the server library barrel through resolvable subpath exports", async () => {
|
|
48
|
+
const packagejson = JSON.parse(await readFile("package.json", "utf8")) as {
|
|
49
|
+
exports: Record<string, Record<string, string>>;
|
|
50
|
+
};
|
|
51
|
+
const serverlib = packagejson.exports["./server"];
|
|
52
|
+
expect(serverlib).toBeDefined();
|
|
53
|
+
expect(serverlib?.types).toBe("./dist/server.d.ts");
|
|
54
|
+
expect(serverlib?.import).toBe("./dist/server.js");
|
|
55
|
+
expect(serverlib?.default).toBe("./dist/server.js");
|
|
56
|
+
/* the library surface is repository sources the build compiles: the source file exists before any
|
|
57
|
+
build and the dist entries join the built set the validate chain asserts */
|
|
58
|
+
if (!existsSync("server.ts")) throw new Error("The library surface server.ts of the grand merge is missing.");
|
|
59
|
+
/* every non-dist exports target resolves unconditionally; the dist targets
|
|
60
|
+
* follow the family convention — the validate chain runs the tests before
|
|
61
|
+
* the build, the existence pass rides the lanes that build first */
|
|
62
|
+
if (!existsSync("dist/checksums.txt")) return;
|
|
63
|
+
const referenced = new Set<string>();
|
|
64
|
+
for (const entry of Object.values(packagejson.exports))
|
|
65
|
+
for (const target of Object.values(entry)) referenced.add(target.replace(/^\.\//, ""));
|
|
66
|
+
for (const file of referenced)
|
|
67
|
+
if (!existsSync(join(process.cwd(), file)))
|
|
68
|
+
throw new Error(`The exports target ${file} does not resolve to a repository file.`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("documents the server and provider library surfaces of the merged package", async () => {
|
|
72
|
+
const serverdoc = await readFile("docs/server-library.md", "utf8");
|
|
73
|
+
expect(serverdoc).toContain("@wenathlan/devthink/server");
|
|
74
|
+
for (const symbol of ["createversion", "createserver", "runserver", "loadconfig", "engineinternals"]) {
|
|
75
|
+
expect(serverdoc).toContain(symbol);
|
|
76
|
+
}
|
|
77
|
+
expect(serverdoc).toContain("12 auth methods");
|
|
78
|
+
expect(serverdoc).toContain("web/vercel.json");
|
|
79
|
+
expect(serverdoc).toContain("web/netlify.toml");
|
|
80
|
+
expect(serverdoc).toContain("Dockerfile");
|
|
81
|
+
|
|
82
|
+
const providerdoc = await readFile("docs/provider-guide.md", "utf8");
|
|
83
|
+
expect(providerdoc).toContain("@wenathlan/devthink/server");
|
|
84
|
+
expect(providerdoc).toContain("oauth.ts");
|
|
85
|
+
expect(providerdoc).toContain("devthink.ts");
|
|
86
|
+
expect(providerdoc).toContain("antigravity.ts");
|
|
87
|
+
expect(providerdoc).toContain("soft_quota_threshold_percent");
|
|
88
|
+
expect(providerdoc).toContain("docs/schemas/antigravity.json");
|
|
89
|
+
expect(providerdoc).toContain("The maene lineage");
|
|
90
|
+
});
|
|
91
|
+
});
|