@wenathlan/devthink 2.0.15 → 2.0.20
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 +54 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/tests/memory.test.ts
DELETED
|
@@ -1,4155 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { sessionmemory } from "../memory.js";
|
|
3
|
-
import type {
|
|
4
|
-
bodyrecord,
|
|
5
|
-
callrecord,
|
|
6
|
-
costbudget,
|
|
7
|
-
lessonrecord,
|
|
8
|
-
plandraft,
|
|
9
|
-
prompttemplate,
|
|
10
|
-
providerconfig,
|
|
11
|
-
reflectnote,
|
|
12
|
-
replanrecord,
|
|
13
|
-
usagerecord,
|
|
14
|
-
} from "../types.js";
|
|
15
|
-
|
|
16
|
-
class fakeadapter {
|
|
17
|
-
private readonly data = new Map<string, unknown>();
|
|
18
|
-
async get<T>(key: string): Promise<T | undefined> {
|
|
19
|
-
return this.data.get(key) as T | undefined;
|
|
20
|
-
}
|
|
21
|
-
async set<T>(key: string, value: T): Promise<void> {
|
|
22
|
-
this.data.set(key, value);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe("sessionmemory", () => {
|
|
27
|
-
it("keeps an ordered audit trail with unlimited default retention", async () => {
|
|
28
|
-
const store = new sessionmemory(new fakeadapter());
|
|
29
|
-
await store.addaudi({ id: "one", kind: "observe", at: 1, summary: "first" });
|
|
30
|
-
await store.addaudi({ id: "two", kind: "stop", at: 2, summary: "second" });
|
|
31
|
-
expect((await store.getaudit()).map((event) => event.id)).toEqual(["two", "one"]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("applies the user configured audit retention window", async () => {
|
|
35
|
-
const store = new sessionmemory(new fakeadapter());
|
|
36
|
-
await store.setsettings({ auditretention: 2 });
|
|
37
|
-
for (const id of ["one", "two", "three"]) await store.addaudi({ id, kind: "observe", at: 1, summary: id });
|
|
38
|
-
expect((await store.getaudit()).map((event) => event.id)).toEqual(["three", "two"]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("keeps one structured diagnostics record", async () => {
|
|
42
|
-
const store = new sessionmemory(new fakeadapter());
|
|
43
|
-
await store.setdiagnostic({
|
|
44
|
-
id: "health",
|
|
45
|
-
sessionid: "session",
|
|
46
|
-
origin: "https://example.com",
|
|
47
|
-
capturedat: 1,
|
|
48
|
-
tabid: 2,
|
|
49
|
-
title: "Example",
|
|
50
|
-
textlength: 34,
|
|
51
|
-
interactivecount: 5,
|
|
52
|
-
formcount: 1,
|
|
53
|
-
bridgeavailable: true,
|
|
54
|
-
});
|
|
55
|
-
expect((await store.getdiagnostic())?.bridgeavailable).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("stores step outcomes with unlimited default retention", async () => {
|
|
59
|
-
const store = new sessionmemory(new fakeadapter());
|
|
60
|
-
await store.addoutcome({ stepid: "one", ok: true, summary: "Done.", details: { count: 1 }, at: 1 });
|
|
61
|
-
await store.addoutcome({ stepid: "two", ok: false, summary: "Failed.", at: 2 });
|
|
62
|
-
const outcomes = await store.getoutcomes();
|
|
63
|
-
expect(outcomes.map((outcome) => outcome.stepid)).toEqual(["two", "one"]);
|
|
64
|
-
expect(outcomes[1]?.details?.count).toBe(1);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("stores the negotiated capability report", async () => {
|
|
68
|
-
const store = new sessionmemory(new fakeadapter());
|
|
69
|
-
await store.setcapabilities({
|
|
70
|
-
tabs: true,
|
|
71
|
-
downloads: false,
|
|
72
|
-
clipboardread: false,
|
|
73
|
-
clipboardwrite: false,
|
|
74
|
-
reportedat: 7,
|
|
75
|
-
});
|
|
76
|
-
expect((await store.getcapabilities())?.tabs).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("stores every clickable map under its observation version", async () => {
|
|
80
|
-
const store = new sessionmemory(new fakeadapter());
|
|
81
|
-
expect(await store.getobservationversion()).toBeUndefined();
|
|
82
|
-
const first = await store.nextobservationversion();
|
|
83
|
-
const second = await store.nextobservationversion();
|
|
84
|
-
expect(first).toBe(1);
|
|
85
|
-
expect(second).toBe(2);
|
|
86
|
-
expect(await store.getobservationversion()).toBe(2);
|
|
87
|
-
await store.setmap({
|
|
88
|
-
version: first,
|
|
89
|
-
builtat: 10,
|
|
90
|
-
entries: [{ number: 1, selector: "#go", role: "button", label: "Go", mode: "selector" }],
|
|
91
|
-
});
|
|
92
|
-
await store.setmap({
|
|
93
|
-
version: second,
|
|
94
|
-
builtat: 20,
|
|
95
|
-
entries: [
|
|
96
|
-
{ number: 1, selector: "#go", role: "button", label: "Go", mode: "selector" },
|
|
97
|
-
{ number: 2, selector: "a:nth-of-type(1)", role: "link", label: "Home", mode: "selector" },
|
|
98
|
-
],
|
|
99
|
-
});
|
|
100
|
-
expect((await store.getmap(first))?.entries).toHaveLength(1);
|
|
101
|
-
expect((await store.getmap(second))?.entries[1]).toMatchObject({
|
|
102
|
-
number: 2,
|
|
103
|
-
selector: "a:nth-of-type(1)",
|
|
104
|
-
role: "link",
|
|
105
|
-
label: "Home",
|
|
106
|
-
});
|
|
107
|
-
expect(await store.getmap(404)).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("keeps the key hold registry with tab and step provenance across writes", async () => {
|
|
111
|
-
const store = new sessionmemory(new fakeadapter());
|
|
112
|
-
expect(await store.getholds()).toEqual([]);
|
|
113
|
-
await store.setholds([
|
|
114
|
-
{ holdid: "shift1", key: "Shift", modifiers: ["shift"], tabid: 4, stepid: "s1", pressedat: 100 },
|
|
115
|
-
{ holdid: "ctrl1", key: "Control", tabid: 4, stepid: "s2", pressedat: 200, releasedat: 260 },
|
|
116
|
-
]);
|
|
117
|
-
const holds = await store.getholds();
|
|
118
|
-
expect(holds).toHaveLength(2);
|
|
119
|
-
expect(holds[0]).toMatchObject({ holdid: "shift1", key: "Shift", tabid: 4, stepid: "s1", pressedat: 100 });
|
|
120
|
-
expect(holds[1]?.releasedat).toBe(260);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("records dialog decisions with the reviewed answer for the audit trail", async () => {
|
|
124
|
-
const store = new sessionmemory(new fakeadapter());
|
|
125
|
-
await store.adddialog({
|
|
126
|
-
id: "d1",
|
|
127
|
-
dialog: "confirm",
|
|
128
|
-
text: "Leave the page?",
|
|
129
|
-
accept: true,
|
|
130
|
-
sessionid: "session",
|
|
131
|
-
at: 10,
|
|
132
|
-
});
|
|
133
|
-
await store.adddialog({
|
|
134
|
-
id: "d2",
|
|
135
|
-
dialog: "prompt",
|
|
136
|
-
text: "Your name?",
|
|
137
|
-
accept: true,
|
|
138
|
-
answer: "devthink",
|
|
139
|
-
sessionid: "session",
|
|
140
|
-
at: 20,
|
|
141
|
-
});
|
|
142
|
-
const dialogs = await store.getdialogs();
|
|
143
|
-
expect(dialogs.map((decision) => decision.id)).toEqual(["d2", "d1"]);
|
|
144
|
-
expect(dialogs[0]).toMatchObject({ dialog: "prompt", accept: true, answer: "devthink" });
|
|
145
|
-
expect(dialogs[1]?.accept).toBe(true);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("records retry outcomes with attempts and movement deltas", async () => {
|
|
149
|
-
const store = new sessionmemory(new fakeadapter());
|
|
150
|
-
await store.addretry({ stepid: "wrap", attempts: 3, movement: 12.5, ok: true, at: 30 });
|
|
151
|
-
await store.addretry({ stepid: "wrap", attempts: 1, movement: 0, ok: false, at: 40 });
|
|
152
|
-
const retries = await store.getretries();
|
|
153
|
-
expect(retries.map((outcome) => outcome.attempts)).toEqual([1, 3]);
|
|
154
|
-
expect(retries[1]).toMatchObject({ stepid: "wrap", movement: 12.5, ok: true });
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("records resolution summaries per target mode for later selector derivation", async () => {
|
|
158
|
-
const store = new sessionmemory(new fakeadapter());
|
|
159
|
-
await store.addresolution({ stepid: "t1", mode: "text", selector: "#signin", label: "Sign in", at: 50 });
|
|
160
|
-
await store.addresolution({ stepid: "a1", mode: "aria", selector: "#submit", label: "Submit", at: 60 });
|
|
161
|
-
const resolutions = await store.getresolutions();
|
|
162
|
-
expect(resolutions.map((summary) => summary.mode)).toEqual(["aria", "text"]);
|
|
163
|
-
expect(resolutions[1]).toMatchObject({ stepid: "t1", selector: "#signin", label: "Sign in" });
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("keeps the reviewed default dialog policy for the session auto handler", async () => {
|
|
167
|
-
const store = new sessionmemory(new fakeadapter());
|
|
168
|
-
expect(await store.getdialogpolicy()).toBeUndefined();
|
|
169
|
-
await store.setdialogpolicy({ accept: true, answer: "devthink" });
|
|
170
|
-
expect(await store.getdialogpolicy()).toEqual({ accept: true, answer: "devthink" });
|
|
171
|
-
await store.setdialogpolicy({ accept: false });
|
|
172
|
-
expect(await store.getdialogpolicy()).toEqual({ accept: false });
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("stores every observation version with its schema fields", async () => {
|
|
176
|
-
const store = new sessionmemory(new fakeadapter());
|
|
177
|
-
const version = await store.nextobservationversion();
|
|
178
|
-
await store.setobservation({
|
|
179
|
-
version,
|
|
180
|
-
observation: {
|
|
181
|
-
schemaversion: 3,
|
|
182
|
-
url: "https://example.com",
|
|
183
|
-
title: "Example",
|
|
184
|
-
textpreview: "text",
|
|
185
|
-
textlength: 4,
|
|
186
|
-
forms: [],
|
|
187
|
-
interactive: [],
|
|
188
|
-
capturedat: 9,
|
|
189
|
-
mode: "passive",
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
const record = await store.getobservation(version);
|
|
193
|
-
expect(record?.version).toBe(1);
|
|
194
|
-
expect(record?.observation.schemaversion).toBe(3);
|
|
195
|
-
expect(record?.observation.mode).toBe("passive");
|
|
196
|
-
expect(await store.getobservation(404)).toBeUndefined();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("stores a11y trees and reader articles with the user configured retention", async () => {
|
|
200
|
-
const store = new sessionmemory(new fakeadapter());
|
|
201
|
-
expect(await store.geta11ytrees()).toEqual([]);
|
|
202
|
-
expect(await store.getreaderarticles()).toEqual([]);
|
|
203
|
-
const tree = { role: "document", name: "", states: [] as string[], childcount: 0, children: [] };
|
|
204
|
-
const article = {
|
|
205
|
-
title: "Deep observation",
|
|
206
|
-
byline: "Ada",
|
|
207
|
-
blocks: [{ kind: "p", text: "text", words: 1 }],
|
|
208
|
-
words: 1,
|
|
209
|
-
characters: 4,
|
|
210
|
-
};
|
|
211
|
-
await store.adda11ytree({ version: 1, tree, capturedat: 10 });
|
|
212
|
-
await store.adda11ytree({ version: 2, tree, capturedat: 20 });
|
|
213
|
-
await store.adda11ytree({ version: 3, tree, capturedat: 30 });
|
|
214
|
-
await store.addreaderarticle({ version: 1, article, capturedat: 10 });
|
|
215
|
-
await store.addreaderarticle({ version: 2, article, capturedat: 20 });
|
|
216
|
-
expect((await store.geta11ytrees()).map((capture) => capture.version)).toEqual([3, 2, 1]);
|
|
217
|
-
expect((await store.getreaderarticles()).map((capture) => capture.version)).toEqual([2, 1]);
|
|
218
|
-
await store.setsettings({ observationretention: 1 });
|
|
219
|
-
await store.adda11ytree({ version: 4, tree, capturedat: 40 });
|
|
220
|
-
await store.addreaderarticle({ version: 3, article, capturedat: 40 });
|
|
221
|
-
expect((await store.geta11ytrees()).map((capture) => capture.version)).toEqual([4]);
|
|
222
|
-
expect((await store.getreaderarticles()).map((capture) => capture.version)).toEqual([3]);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("stores mutation, focus and banner event streams per session", async () => {
|
|
226
|
-
const store = new sessionmemory(new fakeadapter());
|
|
227
|
-
expect(await store.getmutationevents()).toEqual([]);
|
|
228
|
-
expect(await store.getfocusevents()).toEqual([]);
|
|
229
|
-
expect(await store.getbanners()).toEqual([]);
|
|
230
|
-
await store.addmutationevent({
|
|
231
|
-
watchid: "w1",
|
|
232
|
-
event: "childList",
|
|
233
|
-
targetpath: "#feed > li",
|
|
234
|
-
sessionid: "session",
|
|
235
|
-
at: 100,
|
|
236
|
-
});
|
|
237
|
-
await store.addmutationevent({
|
|
238
|
-
watchid: "w1",
|
|
239
|
-
event: "attributes",
|
|
240
|
-
targetpath: "#feed",
|
|
241
|
-
sessionid: "session",
|
|
242
|
-
at: 110,
|
|
243
|
-
});
|
|
244
|
-
await store.addfocusevent({ watchid: "w2", kind: "focus", targetpath: "#search", sessionid: "session", at: 150 });
|
|
245
|
-
await store.addfocusevent({ watchid: "w2", kind: "blur", targetpath: "#search", sessionid: "session", at: 160 });
|
|
246
|
-
await store.addbanner({
|
|
247
|
-
kind: "cookie",
|
|
248
|
-
selector: "#cookiebar",
|
|
249
|
-
text: "We use cookies.",
|
|
250
|
-
controls: ["Accept"],
|
|
251
|
-
sessionid: "session",
|
|
252
|
-
at: 200,
|
|
253
|
-
});
|
|
254
|
-
expect((await store.getmutationevents()).map((event) => event.event)).toEqual(["attributes", "childList"]);
|
|
255
|
-
expect((await store.getfocusevents()).map((event) => event.kind)).toEqual(["blur", "focus"]);
|
|
256
|
-
expect((await store.getbanners())[0]).toMatchObject({ kind: "cookie", controls: ["Accept"], sessionid: "session" });
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("stores snapshot diffs with their two observation versions and derived selectors with stability scores", async () => {
|
|
260
|
-
const store = new sessionmemory(new fakeadapter());
|
|
261
|
-
expect(await store.getdiffs()).toEqual([]);
|
|
262
|
-
expect(await store.getselectors()).toEqual([]);
|
|
263
|
-
await store.adddiff({
|
|
264
|
-
baseversion: 2,
|
|
265
|
-
targetversion: 5,
|
|
266
|
-
added: [{ kind: "added", selector: "#new", summary: "New" }],
|
|
267
|
-
removed: [],
|
|
268
|
-
changed: [{ kind: "changed", selector: "#go", summary: "Go became Stop" }],
|
|
269
|
-
at: 42,
|
|
270
|
-
});
|
|
271
|
-
await store.addselector({ stepid: "s1", selector: "#checkout", strategy: "id", score: 100, at: 50 });
|
|
272
|
-
await store.addselector({
|
|
273
|
-
stepid: "s2",
|
|
274
|
-
selector: 'button[data-testid="checkout"]',
|
|
275
|
-
strategy: "attribute",
|
|
276
|
-
score: 80,
|
|
277
|
-
at: 60,
|
|
278
|
-
});
|
|
279
|
-
const diffs = await store.getdiffs();
|
|
280
|
-
expect(diffs[0]).toMatchObject({ baseversion: 2, targetversion: 5 });
|
|
281
|
-
expect(diffs[0]?.added).toHaveLength(1);
|
|
282
|
-
expect(diffs[0]?.changed).toHaveLength(1);
|
|
283
|
-
const selectors = await store.getselectors();
|
|
284
|
-
expect(selectors.map((record) => record.score)).toEqual([80, 100]);
|
|
285
|
-
expect(selectors[1]).toMatchObject({ stepid: "s1", selector: "#checkout", strategy: "id", score: 100 });
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("stores detected templates and section fingerprints per origin", async () => {
|
|
289
|
-
const store = new sessionmemory(new fakeadapter());
|
|
290
|
-
expect(await store.gettemplates()).toEqual([]);
|
|
291
|
-
await store.addtemplate({ origin: "https://example.com", template: "article", fingerprint: "fp1a", at: 10 });
|
|
292
|
-
await store.addtemplate({
|
|
293
|
-
origin: "https://example.com",
|
|
294
|
-
template: "",
|
|
295
|
-
fingerprint: "fp2b",
|
|
296
|
-
section: "#prices",
|
|
297
|
-
at: 20,
|
|
298
|
-
});
|
|
299
|
-
const templates = await store.gettemplates();
|
|
300
|
-
expect(templates[0]).toMatchObject({ fingerprint: "fp2b", section: "#prices" });
|
|
301
|
-
expect(templates[1]).toMatchObject({ template: "article", fingerprint: "fp1a" });
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("keeps watch registrations across restarts and closes them by watch id", async () => {
|
|
305
|
-
const store = new sessionmemory(new fakeadapter());
|
|
306
|
-
expect(await store.getwatches()).toEqual([]);
|
|
307
|
-
const watch = {
|
|
308
|
-
watchid: "w1",
|
|
309
|
-
kind: "watchmutate" as const,
|
|
310
|
-
stepid: "s1",
|
|
311
|
-
sessionid: "session",
|
|
312
|
-
origin: "https://example.com",
|
|
313
|
-
scopes: ["#feed"],
|
|
314
|
-
events: ["childList"],
|
|
315
|
-
startedat: 1000,
|
|
316
|
-
lifetime: 2500,
|
|
317
|
-
};
|
|
318
|
-
await store.addwatch(watch);
|
|
319
|
-
await store.addwatch({ ...watch, watchid: "w2", kind: "watchfocus", lifetime: 60000 });
|
|
320
|
-
await store.closewatch("w1", 3600);
|
|
321
|
-
const watches = await store.getwatches();
|
|
322
|
-
expect(watches.map((registration) => registration.watchid)).toEqual(["w2", "w1"]);
|
|
323
|
-
expect(watches[1]?.closedat).toBe(3600);
|
|
324
|
-
expect(watches[0]?.closedat).toBeUndefined();
|
|
325
|
-
await store.closewatch("w1", 9999);
|
|
326
|
-
expect((await store.getwatches())[1]?.closedat).toBe(3600);
|
|
327
|
-
await store.closewatch("w2", 9999);
|
|
328
|
-
expect((await store.getwatches())[0]?.closedat).toBe(9999);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("replaces the live page signals after observation steps", async () => {
|
|
332
|
-
const store = new sessionmemory(new fakeadapter());
|
|
333
|
-
expect(await store.getsignals()).toBeUndefined();
|
|
334
|
-
await store.setsignals({ language: "pt-BR", template: "article", refreshedat: 10 });
|
|
335
|
-
expect(await store.getsignals()).toEqual({ language: "pt-BR", template: "article", refreshedat: 10 });
|
|
336
|
-
await store.setsignals({
|
|
337
|
-
language: "pt-BR",
|
|
338
|
-
template: "article",
|
|
339
|
-
scrolllocked: true,
|
|
340
|
-
banner: "cookie",
|
|
341
|
-
refreshedat: 20,
|
|
342
|
-
});
|
|
343
|
-
expect(await store.getsignals()).toMatchObject({ scrolllocked: true, banner: "cookie", refreshedat: 20 });
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("keeps the navigation trail of a session with timestamps and step refs", async () => {
|
|
347
|
-
const store = new sessionmemory(new fakeadapter());
|
|
348
|
-
expect(await store.gettrail("session")).toEqual([]);
|
|
349
|
-
await store.addtrailentry("session", { url: "https://example.com/", title: "Home", stepid: "s1", at: 10 });
|
|
350
|
-
await store.addtrailentry("session", {
|
|
351
|
-
url: "https://example.com/pricing",
|
|
352
|
-
title: "Pricing",
|
|
353
|
-
stepid: "s2",
|
|
354
|
-
at: 20,
|
|
355
|
-
});
|
|
356
|
-
await store.addtrailentry("other", { url: "https://other.example/", title: "Other", at: 30 });
|
|
357
|
-
const trail = await store.gettrail("session");
|
|
358
|
-
expect(trail).toHaveLength(2);
|
|
359
|
-
expect(trail[1]).toMatchObject({ url: "https://example.com/pricing", title: "Pricing", stepid: "s2", at: 20 });
|
|
360
|
-
expect(await store.gettrail("other")).toHaveLength(1);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("stores wait profiles per origin with user configured values", async () => {
|
|
364
|
-
const store = new sessionmemory(new fakeadapter());
|
|
365
|
-
expect(await store.getwaitprofiles()).toEqual([]);
|
|
366
|
-
await store.setwaitprofile({ origin: "https://example.com", profile: { signals: ["load"], idle: 400 }, at: 10 });
|
|
367
|
-
await store.setwaitprofile({
|
|
368
|
-
origin: "https://example.com",
|
|
369
|
-
profile: { signals: ["load", "networkidle"], idle: 900, timeout: 8000 },
|
|
370
|
-
at: 20,
|
|
371
|
-
});
|
|
372
|
-
await store.setwaitprofile({ origin: "https://slow.example", profile: { signals: ["load"], idle: 2000 }, at: 30 });
|
|
373
|
-
const profiles = await store.getwaitprofiles();
|
|
374
|
-
expect(profiles).toHaveLength(2);
|
|
375
|
-
expect(profiles.find((record) => record.origin === "https://example.com")?.profile).toMatchObject({
|
|
376
|
-
idle: 900,
|
|
377
|
-
timeout: 8000,
|
|
378
|
-
});
|
|
379
|
-
expect(profiles.find((record) => record.origin === "https://slow.example")?.profile.signals).toEqual(["load"]);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it("stores redirect chains and final urls per navigation step", async () => {
|
|
383
|
-
const store = new sessionmemory(new fakeadapter());
|
|
384
|
-
expect(await store.getnavrecords()).toEqual([]);
|
|
385
|
-
const chain = {
|
|
386
|
-
hops: [
|
|
387
|
-
{ url: "https://example.com/a", status: 302, at: 10 },
|
|
388
|
-
{ url: "https://example.com/b", status: 200, at: 25 },
|
|
389
|
-
],
|
|
390
|
-
startedat: 10,
|
|
391
|
-
endedat: 25,
|
|
392
|
-
};
|
|
393
|
-
await store.addnavrecord({
|
|
394
|
-
stepid: "s1",
|
|
395
|
-
sessionid: "session",
|
|
396
|
-
origin: "https://example.com",
|
|
397
|
-
finalurl: "https://example.com/b",
|
|
398
|
-
chain,
|
|
399
|
-
at: 30,
|
|
400
|
-
});
|
|
401
|
-
const records = await store.getnavrecords();
|
|
402
|
-
expect(records).toHaveLength(1);
|
|
403
|
-
expect(records[0]).toMatchObject({ stepid: "s1", finalurl: "https://example.com/b" });
|
|
404
|
-
expect(records[0]?.chain.hops[0]).toMatchObject({ status: 302, at: 10 });
|
|
405
|
-
await store.setnavstate(4, records[0]!);
|
|
406
|
-
expect((await store.getnavstate(4))?.stepid).toBe("s1");
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it("stores navintent records, rate limit windows, curated lists and safety verdicts", async () => {
|
|
410
|
-
const store = new sessionmemory(new fakeadapter());
|
|
411
|
-
await store.addnavintent({
|
|
412
|
-
id: "i1",
|
|
413
|
-
intent: "open the pricing page",
|
|
414
|
-
origin: "https://example.com",
|
|
415
|
-
sessionid: "session",
|
|
416
|
-
stepid: "s1",
|
|
417
|
-
at: 10,
|
|
418
|
-
});
|
|
419
|
-
expect((await store.getnavintents())[0]).toMatchObject({
|
|
420
|
-
intent: "open the pricing page",
|
|
421
|
-
origin: "https://example.com",
|
|
422
|
-
});
|
|
423
|
-
await store.setratestate({
|
|
424
|
-
domain: "example.com",
|
|
425
|
-
limit: { domain: "example.com", window: 60000, ceiling: 3 },
|
|
426
|
-
openedat: 100,
|
|
427
|
-
count: 1,
|
|
428
|
-
});
|
|
429
|
-
await store.setratestate({
|
|
430
|
-
domain: "example.com",
|
|
431
|
-
limit: { domain: "example.com", window: 60000, ceiling: 5 },
|
|
432
|
-
openedat: 200,
|
|
433
|
-
count: 0,
|
|
434
|
-
});
|
|
435
|
-
await store.setratestate({
|
|
436
|
-
domain: "other.example",
|
|
437
|
-
limit: { domain: "other.example", window: 30000, ceiling: 2 },
|
|
438
|
-
openedat: 300,
|
|
439
|
-
count: 0,
|
|
440
|
-
});
|
|
441
|
-
const rates = await store.getratestates();
|
|
442
|
-
expect(rates).toHaveLength(2);
|
|
443
|
-
expect(rates.find((state) => state.domain === "example.com")).toMatchObject({ count: 0, openedat: 200 });
|
|
444
|
-
await store.addcurated({
|
|
445
|
-
id: "c1",
|
|
446
|
-
links: [
|
|
447
|
-
{ url: "https://example.com/a", verdict: "safe", reasons: [] },
|
|
448
|
-
{ url: "http://bad.example", verdict: "unsafe", reasons: ["the url must use HTTPS"] },
|
|
449
|
-
],
|
|
450
|
-
at: 10,
|
|
451
|
-
});
|
|
452
|
-
const curated = await store.getcurateds();
|
|
453
|
-
expect(curated[0]?.links[1]).toMatchObject({ verdict: "unsafe" });
|
|
454
|
-
await store.addcurated({ ...curated[0]!, reviewedat: 20 });
|
|
455
|
-
expect((await store.getcurateds())[0]?.reviewedat).toBe(20);
|
|
456
|
-
await store.addsafety({ url: "https://partner.example", safe: true, reasons: [], at: 40 });
|
|
457
|
-
expect((await store.getsafeties())[0]).toMatchObject({ url: "https://partner.example", safe: true });
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it("stores reviewed auth records, artifacts, nav control, recent tabs and queue counts", async () => {
|
|
461
|
-
const store = new sessionmemory(new fakeadapter());
|
|
462
|
-
await store.setauth({ origin: "https://example.com", username: "devthink", password: "secret", reviewedat: 10 });
|
|
463
|
-
await store.setauth({ origin: "https://example.com", username: "reviewed", password: "changed", reviewedat: 20 });
|
|
464
|
-
await store.setauth({ origin: "https://other.example", username: "other", password: "pass", reviewedat: 30 });
|
|
465
|
-
const auths = await store.getauths();
|
|
466
|
-
expect(auths).toHaveLength(2);
|
|
467
|
-
expect(auths.find((record) => record.origin === "https://example.com")?.username).toBe("reviewed");
|
|
468
|
-
await store.addartifact({ id: "a1", kind: "printpdf", name: "report.pdf", stepid: "s1", at: 40 });
|
|
469
|
-
expect((await store.getartifacts())[0]).toMatchObject({ kind: "printpdf", name: "report.pdf" });
|
|
470
|
-
expect(await store.getnavcontrol()).toBeUndefined();
|
|
471
|
-
await store.setnavcontrol({ pausedat: 50, reason: "a consent banner is open", updatedat: 50 });
|
|
472
|
-
expect(await store.getnavcontrol()).toMatchObject({ pausedat: 50 });
|
|
473
|
-
await store.setnavcontrol({ updatedat: 60 });
|
|
474
|
-
expect((await store.getnavcontrol())?.pausedat).toBeUndefined();
|
|
475
|
-
await store.addrecenttab({ url: "https://example.com/closed", tabid: 4, closedat: 70 });
|
|
476
|
-
expect((await store.getrecenttabs())[0]).toMatchObject({ url: "https://example.com/closed", tabid: 4 });
|
|
477
|
-
await store.setnavqueues({ prefetch: 2, batchopen: 1, updatedat: 80 });
|
|
478
|
-
expect(await store.getnavqueues()).toMatchObject({ prefetch: 2, batchopen: 1 });
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
describe("sessionmemory tabs and windows command", () => {
|
|
483
|
-
it("stores named tab layouts with their timestamps and window bounds", async () => {
|
|
484
|
-
const store = new sessionmemory(new fakeadapter());
|
|
485
|
-
const layout = {
|
|
486
|
-
name: "work",
|
|
487
|
-
tabs: [{ url: "https://example.com/a", title: "A", pinned: true, index: 0, windowid: 10 }],
|
|
488
|
-
groups: [],
|
|
489
|
-
windows: [
|
|
490
|
-
{
|
|
491
|
-
windowid: 10,
|
|
492
|
-
state: {
|
|
493
|
-
bounds: { left: 0, top: 0, width: 1280, height: 800 },
|
|
494
|
-
maximized: false,
|
|
495
|
-
profile: "normal" as const,
|
|
496
|
-
},
|
|
497
|
-
},
|
|
498
|
-
],
|
|
499
|
-
savedat: 10,
|
|
500
|
-
};
|
|
501
|
-
await store.setlayout(layout);
|
|
502
|
-
await store.setlayout({ ...layout, savedat: 20 });
|
|
503
|
-
await store.setlayout({ ...layout, name: "research", savedat: 30 });
|
|
504
|
-
expect((await store.getlayout("work"))?.savedat).toBe(20);
|
|
505
|
-
expect((await store.getlayouts()).map((item) => item.name)).toEqual(["research", "work"]);
|
|
506
|
-
expect(await store.getlayout("missing")).toBeUndefined();
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("stores tabgroup definitions with their color choices and member tabs", async () => {
|
|
510
|
-
const store = new sessionmemory(new fakeadapter());
|
|
511
|
-
await store.settabgroup({
|
|
512
|
-
groupid: "g1",
|
|
513
|
-
name: "research",
|
|
514
|
-
color: "blue",
|
|
515
|
-
tabids: [4, 5],
|
|
516
|
-
collapsed: false,
|
|
517
|
-
savedat: 10,
|
|
518
|
-
});
|
|
519
|
-
await store.settabgroup({
|
|
520
|
-
groupid: "g2",
|
|
521
|
-
name: "research",
|
|
522
|
-
color: "pink",
|
|
523
|
-
tabids: [4],
|
|
524
|
-
collapsed: true,
|
|
525
|
-
savedat: 20,
|
|
526
|
-
});
|
|
527
|
-
await store.settabgroup({ groupid: "g3", name: "docs", color: "grey", tabids: [6], collapsed: false, savedat: 30 });
|
|
528
|
-
const groups = await store.gettabgroups();
|
|
529
|
-
expect(groups).toHaveLength(2);
|
|
530
|
-
expect(groups.find((group) => group.name === "research")).toMatchObject({
|
|
531
|
-
color: "pink",
|
|
532
|
-
collapsed: true,
|
|
533
|
-
tabids: [4],
|
|
534
|
-
});
|
|
535
|
-
expect(groups.find((group) => group.name === "docs")?.tabids).toEqual([6]);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("stores tabmeta records with task provenance per tab", async () => {
|
|
539
|
-
const store = new sessionmemory(new fakeadapter());
|
|
540
|
-
await store.settabmeta({ tabid: 4, taskrefs: ["plan-1"], provenance: "plan step", labels: ["research"], at: 10 });
|
|
541
|
-
await store.settabmeta({ tabid: 4, taskrefs: ["plan-2"], provenance: "user review", labels: ["final"], at: 20 });
|
|
542
|
-
await store.settabmeta({ tabid: 5, taskrefs: [], provenance: "plan step", labels: [], at: 30 });
|
|
543
|
-
const metas = await store.gettabmetas();
|
|
544
|
-
expect(metas).toHaveLength(2);
|
|
545
|
-
expect(metas.find((meta) => meta.tabid === 4)).toMatchObject({
|
|
546
|
-
taskrefs: ["plan-2"],
|
|
547
|
-
labels: ["final"],
|
|
548
|
-
provenance: "user review",
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it("stores session snapshots and the closed tab history for later restore", async () => {
|
|
553
|
-
const store = new sessionmemory(new fakeadapter());
|
|
554
|
-
const layout = {
|
|
555
|
-
name: "run",
|
|
556
|
-
tabs: [{ url: "https://example.com/a", title: "A", pinned: false, index: 0, windowid: 10 }],
|
|
557
|
-
groups: [],
|
|
558
|
-
windows: [],
|
|
559
|
-
savedat: 10,
|
|
560
|
-
};
|
|
561
|
-
await store.addsnapshot({ id: "s1", sessionid: "run-1", layout, capturedat: 10 });
|
|
562
|
-
await store.addsnapshot({ id: "s2", sessionid: "run-2", layout, capturedat: 20 });
|
|
563
|
-
const snapshots = await store.getsnapshots();
|
|
564
|
-
expect(snapshots.map((snapshot) => snapshot.id)).toEqual(["s2", "s1"]);
|
|
565
|
-
expect(snapshots[1]).toMatchObject({ sessionid: "run-1", capturedat: 10 });
|
|
566
|
-
await store.addclosedtab({
|
|
567
|
-
url: "https://example.com/older",
|
|
568
|
-
title: "Older",
|
|
569
|
-
tabid: 8,
|
|
570
|
-
windowid: 10,
|
|
571
|
-
closedat: 25,
|
|
572
|
-
});
|
|
573
|
-
await store.addclosedtab({
|
|
574
|
-
url: "https://example.com/closed",
|
|
575
|
-
title: "Closed",
|
|
576
|
-
tabid: 7,
|
|
577
|
-
windowid: 10,
|
|
578
|
-
closedat: 30,
|
|
579
|
-
});
|
|
580
|
-
expect((await store.getclosedtabs())[0]).toMatchObject({ url: "https://example.com/closed", tabid: 7 });
|
|
581
|
-
expect((await store.getclosedtabs())[1]?.url).toBe("https://example.com/older");
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it("stores badge states per task and the watchtab event stream", async () => {
|
|
585
|
-
const store = new sessionmemory(new fakeadapter());
|
|
586
|
-
await store.setbadge({ tabid: 4, taskid: "plan", label: "2/5", setat: 10 });
|
|
587
|
-
await store.setbadge({ tabid: 5, taskid: "plan", label: "done", setat: 20 });
|
|
588
|
-
await store.setbadge({ tabid: 4, taskid: "plan", label: "done", setat: 30 });
|
|
589
|
-
const badges = await store.getbadges();
|
|
590
|
-
expect(badges).toHaveLength(1);
|
|
591
|
-
expect(badges[0]).toMatchObject({ tabid: 4, taskid: "plan", label: "done" });
|
|
592
|
-
await store.addtabwatchevent({ watchid: "w1", event: "title", tabid: 4, detail: "New title", at: 40 });
|
|
593
|
-
await store.addtabwatchevent({ watchid: "w1", event: "closed", tabid: 5, at: 41 });
|
|
594
|
-
const events = await store.gettabwatchevents();
|
|
595
|
-
expect(events).toHaveLength(2);
|
|
596
|
-
expect(events[0]).toMatchObject({ watchid: "w1", event: "closed", tabid: 5 });
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
it("stores scratch window ids and the pinned control tab state", async () => {
|
|
600
|
-
const store = new sessionmemory(new fakeadapter());
|
|
601
|
-
await store.setscratchwindows([11]);
|
|
602
|
-
await store.setscratchwindows([11, 12]);
|
|
603
|
-
expect(await store.getscratchwindows()).toEqual([11, 12]);
|
|
604
|
-
await store.setcontroltab({ tabid: 9, enabled: true, updatedat: 10 });
|
|
605
|
-
expect(await store.getcontroltab()).toMatchObject({ tabid: 9, enabled: true });
|
|
606
|
-
await store.setcontroltab({ tabid: 0, enabled: false, updatedat: 20 });
|
|
607
|
-
expect((await store.getcontroltab())?.enabled).toBe(false);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it("stores form profiles with origin grants, replaces them by name and removes them", async () => {
|
|
611
|
-
const store = new sessionmemory(new fakeadapter());
|
|
612
|
-
const profile = {
|
|
613
|
-
name: "personal",
|
|
614
|
-
fields: [{ match: { mode: "label" as const, label: "Full name" }, kind: "text" as const, value: "Ana Alves" }],
|
|
615
|
-
grants: ["https://example.com"],
|
|
616
|
-
savedat: 10,
|
|
617
|
-
};
|
|
618
|
-
await store.setprofile(profile);
|
|
619
|
-
await store.setprofile({ ...profile, name: "work", savedat: 20 });
|
|
620
|
-
expect(await store.getprofile("work")).toMatchObject({ name: "work", savedat: 20 });
|
|
621
|
-
expect((await store.getprofiles()).map((item) => item.name)).toEqual(["work", "personal"]);
|
|
622
|
-
await store.setprofile({
|
|
623
|
-
...profile,
|
|
624
|
-
fields: [
|
|
625
|
-
...profile.fields,
|
|
626
|
-
{ match: { mode: "name" as const, name: "email" }, kind: "email" as const, value: "a@b.c" },
|
|
627
|
-
],
|
|
628
|
-
savedat: 30,
|
|
629
|
-
});
|
|
630
|
-
expect(await store.getprofiles()).toHaveLength(2);
|
|
631
|
-
expect((await store.getprofile("personal"))?.fields).toHaveLength(2);
|
|
632
|
-
await store.removeprofile("personal");
|
|
633
|
-
expect(await store.getprofile("personal")).toBeUndefined();
|
|
634
|
-
expect((await store.getprofiles()).map((item) => item.name)).toEqual(["work"]);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it("stores wizard states, submission tickets, error reports, picks, captcha handoffs, detections and the reviewed code", async () => {
|
|
638
|
-
const store = new sessionmemory(new fakeadapter());
|
|
639
|
-
await store.addwizard({ index: 1, steps: 3, completed: [true], at: 1 });
|
|
640
|
-
await store.addwizard({ index: 2, steps: 3, completed: [true, true], at: 2 });
|
|
641
|
-
expect((await store.getwizards())[0]).toMatchObject({ index: 2, steps: 3 });
|
|
642
|
-
await store.setticket({ id: "t1", form: "#checkout", valueshash: "abc", consentref: "ask", at: 3 });
|
|
643
|
-
await store.setticket({ id: "t1", form: "#checkout", valueshash: "abc", consentref: "ask", approved: true, at: 3 });
|
|
644
|
-
expect(await store.gettickets()).toHaveLength(1);
|
|
645
|
-
expect((await store.gettickets())[0]?.approved).toBe(true);
|
|
646
|
-
await store.adderrorreport({ form: "#checkout", errors: [{ field: "#email", message: "Invalid email." }], at: 4 });
|
|
647
|
-
expect((await store.geterrorreports())[0]?.errors).toHaveLength(1);
|
|
648
|
-
await store.addpick({ field: "#city", query: "sao", pick: "São Paulo", at: 5 });
|
|
649
|
-
expect((await store.getpicks())[0]?.pick).toBe("São Paulo");
|
|
650
|
-
await store.addcaptcha({ id: "c1", origin: "https://example.com", resolved: false, openedat: 6 });
|
|
651
|
-
await store.resolvecaptcha("c1", 7);
|
|
652
|
-
expect((await store.getcaptchas())[0]).toMatchObject({ resolved: true, resolvedat: 7 });
|
|
653
|
-
await store.adddetection({ origin: "https://example.com", kind: "login", markers: ["password field"], at: 8 });
|
|
654
|
-
expect((await store.getdetections())[0]?.kind).toBe("login");
|
|
655
|
-
await store.setcodevalue("123456");
|
|
656
|
-
expect(await store.getcodevalue()).toBe("123456");
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it("stores datasets, imports, extract sessions, streams, provenance, task rules and sheet endpoints", async () => {
|
|
660
|
-
const store = new sessionmemory(new fakeadapter());
|
|
661
|
-
const columns = [{ key: "sku", label: "Sku", kind: "text" as const, normalized: "sku" }];
|
|
662
|
-
const rows = [{ sku: "a" }, { sku: "b" }];
|
|
663
|
-
await store.setdataset({ id: "d2", name: "second", columns, rows, sources: [], at: 2 });
|
|
664
|
-
await store.setdataset({
|
|
665
|
-
id: "d1",
|
|
666
|
-
name: "catalog",
|
|
667
|
-
columns,
|
|
668
|
-
rows,
|
|
669
|
-
sources: [{ row: 0, url: "https://example.com", at: 1, stepid: "s1" }],
|
|
670
|
-
at: 3,
|
|
671
|
-
});
|
|
672
|
-
expect((await store.getdataset("d1"))?.name).toBe("catalog");
|
|
673
|
-
expect((await store.getdatasets()).map((item) => item.id)).toEqual(["d1", "d2"]);
|
|
674
|
-
await store.addimport({ id: "i1", name: "leads", columns, rows, sources: [], at: 4 });
|
|
675
|
-
expect((await store.getimports()).map((item) => item.id)).toEqual(["i1"]);
|
|
676
|
-
expect((await store.getdataset("i1"))?.name).toBe("leads");
|
|
677
|
-
await store.setextractsession({
|
|
678
|
-
id: "e1",
|
|
679
|
-
datasetid: "d1",
|
|
680
|
-
name: "catalog",
|
|
681
|
-
target: "table",
|
|
682
|
-
next: "a.next",
|
|
683
|
-
planned: 3,
|
|
684
|
-
pages: ["https://example.com/1"],
|
|
685
|
-
rows: 10,
|
|
686
|
-
cursor: 1,
|
|
687
|
-
startedat: 5,
|
|
688
|
-
updatedat: 6,
|
|
689
|
-
});
|
|
690
|
-
expect((await store.getextractsessions())[0]).toMatchObject({ id: "e1", cursor: 1, rows: 10, planned: 3 });
|
|
691
|
-
await store.setstream({ datasetid: "d1", name: "catalog", chunk: 1, chunks: 3, written: 4, at: 7 });
|
|
692
|
-
await store.setstream({ datasetid: "d1", name: "catalog", chunk: 2, chunks: 3, written: 8, at: 8 });
|
|
693
|
-
expect((await store.getstreams())[0]).toMatchObject({ chunk: 2, written: 8 });
|
|
694
|
-
await store.addprovenance({
|
|
695
|
-
artifact: "a1",
|
|
696
|
-
name: "catalog.csv",
|
|
697
|
-
url: "https://example.com",
|
|
698
|
-
stepid: "s2",
|
|
699
|
-
rowstart: 1,
|
|
700
|
-
rowend: 2,
|
|
701
|
-
checksum: "fnv1a-1",
|
|
702
|
-
at: 9,
|
|
703
|
-
});
|
|
704
|
-
expect((await store.getprovenances())[0]).toMatchObject({ artifact: "a1", rowend: 2 });
|
|
705
|
-
await store.settaskrules({
|
|
706
|
-
taskid: "plan-1",
|
|
707
|
-
transforms: [{ expression: "trim", sources: ["sku"], target: "sku" }],
|
|
708
|
-
dedupekeys: [],
|
|
709
|
-
at: 10,
|
|
710
|
-
});
|
|
711
|
-
await store.settaskrules({
|
|
712
|
-
taskid: "plan-1",
|
|
713
|
-
transforms: [{ expression: "trim", sources: ["sku"], target: "sku" }],
|
|
714
|
-
dedupekeys: ["sku"],
|
|
715
|
-
at: 11,
|
|
716
|
-
});
|
|
717
|
-
const rules = (await store.gettaskrules())[0]!;
|
|
718
|
-
expect(rules.dedupekeys).toEqual(["sku"]);
|
|
719
|
-
expect(rules.transforms).toHaveLength(1);
|
|
720
|
-
await store.settaskrules({ taskid: "plan-2", transforms: [], dedupekeys: ["price"], at: 12 });
|
|
721
|
-
expect(await store.gettaskrules()).toHaveLength(2);
|
|
722
|
-
await store.setsheetendpoint({
|
|
723
|
-
endpoint: "https://sheets.example/tab",
|
|
724
|
-
origin: "https://sheets.example",
|
|
725
|
-
configuredat: 12,
|
|
726
|
-
});
|
|
727
|
-
expect((await store.getsheetendpoints())[0]?.origin).toBe("https://sheets.example");
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
it("applies artifact retention to exported artifacts and keeps everything without the setting", async () => {
|
|
731
|
-
const openstore = new sessionmemory(new fakeadapter());
|
|
732
|
-
await openstore.setsettings({});
|
|
733
|
-
await openstore.addexport({
|
|
734
|
-
id: "a1",
|
|
735
|
-
kind: "csv",
|
|
736
|
-
name: "one.csv",
|
|
737
|
-
stepid: "s1",
|
|
738
|
-
rowcount: 1,
|
|
739
|
-
content: "x",
|
|
740
|
-
checksum: "c1",
|
|
741
|
-
at: 1,
|
|
742
|
-
});
|
|
743
|
-
await openstore.addexport({
|
|
744
|
-
id: "a2",
|
|
745
|
-
kind: "json",
|
|
746
|
-
name: "two.json",
|
|
747
|
-
stepid: "s1",
|
|
748
|
-
rowcount: 2,
|
|
749
|
-
content: "y",
|
|
750
|
-
checksum: "c2",
|
|
751
|
-
at: 2,
|
|
752
|
-
});
|
|
753
|
-
expect((await openstore.getexports()).map((item) => item.id)).toEqual(["a2", "a1"]);
|
|
754
|
-
await openstore.addexport({
|
|
755
|
-
id: "a3",
|
|
756
|
-
kind: "excel",
|
|
757
|
-
name: "three.xml",
|
|
758
|
-
stepid: "s1",
|
|
759
|
-
rowcount: 3,
|
|
760
|
-
content: "z",
|
|
761
|
-
checksum: "c3",
|
|
762
|
-
at: 3,
|
|
763
|
-
});
|
|
764
|
-
expect(await openstore.getexports()).toHaveLength(3);
|
|
765
|
-
const bounded = new sessionmemory(new fakeadapter());
|
|
766
|
-
await bounded.setsettings({ artifactretention: 2 });
|
|
767
|
-
await bounded.addexport({
|
|
768
|
-
id: "a1",
|
|
769
|
-
kind: "csv",
|
|
770
|
-
name: "one.csv",
|
|
771
|
-
stepid: "s1",
|
|
772
|
-
rowcount: 1,
|
|
773
|
-
content: "x",
|
|
774
|
-
checksum: "c1",
|
|
775
|
-
at: 1,
|
|
776
|
-
});
|
|
777
|
-
await bounded.addexport({
|
|
778
|
-
id: "a2",
|
|
779
|
-
kind: "csv",
|
|
780
|
-
name: "two.csv",
|
|
781
|
-
stepid: "s1",
|
|
782
|
-
rowcount: 2,
|
|
783
|
-
content: "y",
|
|
784
|
-
checksum: "c2",
|
|
785
|
-
at: 2,
|
|
786
|
-
});
|
|
787
|
-
await bounded.addexport({
|
|
788
|
-
id: "a3",
|
|
789
|
-
kind: "csv",
|
|
790
|
-
name: "three.csv",
|
|
791
|
-
stepid: "s1",
|
|
792
|
-
rowcount: 3,
|
|
793
|
-
content: "z",
|
|
794
|
-
checksum: "c3",
|
|
795
|
-
at: 3,
|
|
796
|
-
});
|
|
797
|
-
expect((await bounded.getexports()).map((item) => item.id)).toEqual(["a3", "a2"]);
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
describe("sessionmemory files, clipboard and downloads", () => {
|
|
802
|
-
it("stores downloads, netlogs with retention, clip consents, clips and quarantines", async () => {
|
|
803
|
-
const store = new sessionmemory(new fakeadapter());
|
|
804
|
-
await store.setdownload({
|
|
805
|
-
id: "d1",
|
|
806
|
-
url: "https://example.com/a.pdf",
|
|
807
|
-
filename: "a.pdf",
|
|
808
|
-
state: "running",
|
|
809
|
-
downloadid: 3,
|
|
810
|
-
at: 1,
|
|
811
|
-
updatedat: 1,
|
|
812
|
-
});
|
|
813
|
-
await store.setdownload({
|
|
814
|
-
id: "d2",
|
|
815
|
-
url: "https://example.com/b.zip",
|
|
816
|
-
filename: "b.zip",
|
|
817
|
-
state: "complete",
|
|
818
|
-
path: "/downloads/b.zip",
|
|
819
|
-
bytes: 10,
|
|
820
|
-
checksum: "fnv1a-1",
|
|
821
|
-
at: 2,
|
|
822
|
-
updatedat: 3,
|
|
823
|
-
});
|
|
824
|
-
await store.setdownload({
|
|
825
|
-
id: "d1",
|
|
826
|
-
url: "https://example.com/a.pdf",
|
|
827
|
-
filename: "a.pdf",
|
|
828
|
-
state: "paused",
|
|
829
|
-
downloadid: 3,
|
|
830
|
-
at: 1,
|
|
831
|
-
updatedat: 4,
|
|
832
|
-
});
|
|
833
|
-
expect((await store.getdownloads())[0]).toMatchObject({ id: "d1", state: "paused" });
|
|
834
|
-
await store.addnetlog({
|
|
835
|
-
url: "https://example.com/list",
|
|
836
|
-
method: "GET",
|
|
837
|
-
status: 200,
|
|
838
|
-
timing: 10,
|
|
839
|
-
requestid: "nav-1-1",
|
|
840
|
-
stepid: "nav-1",
|
|
841
|
-
at: 5,
|
|
842
|
-
});
|
|
843
|
-
await store.addnetlog({
|
|
844
|
-
url: "https://example.com/cart",
|
|
845
|
-
method: "GET",
|
|
846
|
-
status: 200,
|
|
847
|
-
timing: 20,
|
|
848
|
-
requestid: "nav-1-2",
|
|
849
|
-
stepid: "nav-1",
|
|
850
|
-
at: 6,
|
|
851
|
-
});
|
|
852
|
-
expect(await store.getnetlog()).toHaveLength(2);
|
|
853
|
-
await store.setclipconsent({
|
|
854
|
-
id: "clip-1",
|
|
855
|
-
prompt: "Read the tracking number.",
|
|
856
|
-
origin: "https://example.com",
|
|
857
|
-
stepid: "s1",
|
|
858
|
-
at: 7,
|
|
859
|
-
});
|
|
860
|
-
await store.setclipconsent({
|
|
861
|
-
id: "clip-1",
|
|
862
|
-
prompt: "Read the tracking number.",
|
|
863
|
-
origin: "https://example.com",
|
|
864
|
-
stepid: "s1",
|
|
865
|
-
approved: true,
|
|
866
|
-
at: 7,
|
|
867
|
-
});
|
|
868
|
-
expect((await store.getclipconsents())[0]).toMatchObject({ id: "clip-1", approved: true });
|
|
869
|
-
await store.addclip({
|
|
870
|
-
kind: "read",
|
|
871
|
-
hash: "fnv1a-9",
|
|
872
|
-
length: 16,
|
|
873
|
-
origin: "https://example.com",
|
|
874
|
-
stepid: "s1",
|
|
875
|
-
at: 8,
|
|
876
|
-
});
|
|
877
|
-
expect((await store.getclips())[0]).toMatchObject({ kind: "read", hash: "fnv1a-9", length: 16 });
|
|
878
|
-
await store.setquarantine({
|
|
879
|
-
id: "q1",
|
|
880
|
-
path: "devthink-quarantine/x.pdf",
|
|
881
|
-
reason: "mime matched",
|
|
882
|
-
scan: "pending",
|
|
883
|
-
at: 9,
|
|
884
|
-
updatedat: 9,
|
|
885
|
-
});
|
|
886
|
-
await store.setquarantine({
|
|
887
|
-
id: "q1",
|
|
888
|
-
path: "devthink-quarantine/x.pdf",
|
|
889
|
-
reason: "mime matched",
|
|
890
|
-
scan: "clean",
|
|
891
|
-
release: "user-1",
|
|
892
|
-
at: 9,
|
|
893
|
-
updatedat: 10,
|
|
894
|
-
});
|
|
895
|
-
expect((await store.getquarantines())[0]).toMatchObject({ scan: "clean", release: "user-1" });
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
it("applies netlog retention and keeps every record without the setting", async () => {
|
|
899
|
-
const openstore = new sessionmemory(new fakeadapter());
|
|
900
|
-
await openstore.setsettings({});
|
|
901
|
-
await openstore.addnetlog({
|
|
902
|
-
url: "https://example.com/1",
|
|
903
|
-
method: "GET",
|
|
904
|
-
status: 200,
|
|
905
|
-
timing: 1,
|
|
906
|
-
stepid: "s1",
|
|
907
|
-
at: 1,
|
|
908
|
-
});
|
|
909
|
-
await openstore.addnetlog({
|
|
910
|
-
url: "https://example.com/2",
|
|
911
|
-
method: "GET",
|
|
912
|
-
status: 200,
|
|
913
|
-
timing: 2,
|
|
914
|
-
stepid: "s1",
|
|
915
|
-
at: 2,
|
|
916
|
-
});
|
|
917
|
-
await openstore.addnetlog({
|
|
918
|
-
url: "https://example.com/3",
|
|
919
|
-
method: "GET",
|
|
920
|
-
status: 200,
|
|
921
|
-
timing: 3,
|
|
922
|
-
stepid: "s1",
|
|
923
|
-
at: 3,
|
|
924
|
-
});
|
|
925
|
-
expect(await openstore.getnetlog()).toHaveLength(3);
|
|
926
|
-
const bounded = new sessionmemory(new fakeadapter());
|
|
927
|
-
await bounded.setsettings({ netlogretention: 2 });
|
|
928
|
-
await bounded.addnetlog({
|
|
929
|
-
url: "https://example.com/1",
|
|
930
|
-
method: "GET",
|
|
931
|
-
status: 200,
|
|
932
|
-
timing: 1,
|
|
933
|
-
stepid: "s1",
|
|
934
|
-
at: 1,
|
|
935
|
-
});
|
|
936
|
-
await bounded.addnetlog({
|
|
937
|
-
url: "https://example.com/2",
|
|
938
|
-
method: "GET",
|
|
939
|
-
status: 200,
|
|
940
|
-
timing: 2,
|
|
941
|
-
stepid: "s1",
|
|
942
|
-
at: 2,
|
|
943
|
-
});
|
|
944
|
-
await bounded.addnetlog({
|
|
945
|
-
url: "https://example.com/3",
|
|
946
|
-
method: "GET",
|
|
947
|
-
status: 200,
|
|
948
|
-
timing: 3,
|
|
949
|
-
stepid: "s1",
|
|
950
|
-
at: 3,
|
|
951
|
-
});
|
|
952
|
-
expect((await bounded.getnetlog()).map((record) => record.url)).toEqual([
|
|
953
|
-
"https://example.com/3",
|
|
954
|
-
"https://example.com/2",
|
|
955
|
-
]);
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
it("stores cleanup rules, run history, capture counters, inventories, scan hooks and mime filters", async () => {
|
|
959
|
-
const store = new sessionmemory(new fakeadapter());
|
|
960
|
-
await store.setcleanuprules([{ age: 60000, kind: "any", keep: "latest" }]);
|
|
961
|
-
expect(await store.getcleanuprules()).toEqual([{ age: 60000, kind: "any", keep: "latest" }]);
|
|
962
|
-
await store.addcleanuprun({ id: "r1", rules: 1, removed: 2, kept: 5, at: 1 });
|
|
963
|
-
expect((await store.getcleanupruns())[0]).toMatchObject({ removed: 2, kept: 5 });
|
|
964
|
-
await store.setcapturecounter({ taskid: "task-1", counters: { snap: 2 }, at: 2 });
|
|
965
|
-
await store.setcapturecounter({ taskid: "task-1", counters: { snap: 3 }, at: 3 });
|
|
966
|
-
await store.setcapturecounter({ taskid: "task-2", counters: { peek: 1 }, at: 4 });
|
|
967
|
-
expect((await store.getcapturecounters()).map((counter) => counter.taskid)).toEqual(["task-2", "task-1"]);
|
|
968
|
-
expect((await store.getcapturecounters())[1]?.counters).toEqual({ snap: 3 });
|
|
969
|
-
await store.setinventory([{ id: "a1", kind: "export-csv", name: "old.csv", size: 10, at: 5 }]);
|
|
970
|
-
expect((await store.getinventory())[0]).toMatchObject({ name: "old.csv", size: 10 });
|
|
971
|
-
await store.setscanhook({
|
|
972
|
-
scanner: "clamav",
|
|
973
|
-
endpoint: "https://scanner.example/verdict",
|
|
974
|
-
origin: "https://scanner.example",
|
|
975
|
-
configuredat: 6,
|
|
976
|
-
});
|
|
977
|
-
expect((await store.getscanhooks())[0]).toMatchObject({ scanner: "clamav" });
|
|
978
|
-
await store.setscanhook({
|
|
979
|
-
scanner: "second",
|
|
980
|
-
endpoint: "https://other.example/v",
|
|
981
|
-
origin: "https://other.example",
|
|
982
|
-
configuredat: 7,
|
|
983
|
-
});
|
|
984
|
-
await store.setscanhook({
|
|
985
|
-
scanner: "clamav",
|
|
986
|
-
endpoint: "https://scanner.example/v2",
|
|
987
|
-
origin: "https://scanner.example",
|
|
988
|
-
configuredat: 8,
|
|
989
|
-
});
|
|
990
|
-
expect(await store.getscanhooks()).toHaveLength(2);
|
|
991
|
-
await store.setmimefilters([{ include: ["application/pdf*"], exclude: [], default: "deny" }]);
|
|
992
|
-
expect((await store.getmimefilters())[0]?.default).toBe("deny");
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
it("removes exported artifacts and run store artifacts for cleanup sweeps", async () => {
|
|
996
|
-
const store = new sessionmemory(new fakeadapter());
|
|
997
|
-
await store.addexport({
|
|
998
|
-
id: "a1",
|
|
999
|
-
kind: "csv",
|
|
1000
|
-
name: "one.csv",
|
|
1001
|
-
stepid: "s1",
|
|
1002
|
-
rowcount: 1,
|
|
1003
|
-
content: "x",
|
|
1004
|
-
checksum: "c1",
|
|
1005
|
-
at: 1,
|
|
1006
|
-
});
|
|
1007
|
-
await store.addexport({
|
|
1008
|
-
id: "a2",
|
|
1009
|
-
kind: "csv",
|
|
1010
|
-
name: "two.csv",
|
|
1011
|
-
stepid: "s1",
|
|
1012
|
-
rowcount: 2,
|
|
1013
|
-
content: "yy",
|
|
1014
|
-
checksum: "c2",
|
|
1015
|
-
at: 2,
|
|
1016
|
-
});
|
|
1017
|
-
expect(await store.removeexport("a1")).toBe(true);
|
|
1018
|
-
expect(await store.removeexport("missing")).toBe(false);
|
|
1019
|
-
expect((await store.getexports()).map((artifact) => artifact.id)).toEqual(["a2"]);
|
|
1020
|
-
await store.addartifact({ id: "r1", kind: "printpdf", name: "receipt.pdf", stepid: "s2", at: 3 });
|
|
1021
|
-
expect(await store.removeartifact("r1")).toBe(true);
|
|
1022
|
-
expect(await store.removeartifact("r1")).toBe(false);
|
|
1023
|
-
expect(await store.getartifacts()).toEqual([]);
|
|
1024
|
-
});
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
describe("capture memory", () => {
|
|
1028
|
-
it("stores capture records with bytes, filters by run, step and kind, and returns pairs of one run", async () => {
|
|
1029
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1030
|
-
await store.addcapture({
|
|
1031
|
-
id: "c1",
|
|
1032
|
-
runid: "run-1",
|
|
1033
|
-
stepid: "s1",
|
|
1034
|
-
kind: "shotview",
|
|
1035
|
-
format: "png",
|
|
1036
|
-
width: 1280,
|
|
1037
|
-
height: 800,
|
|
1038
|
-
capturedat: 1,
|
|
1039
|
-
bytes: "data:image/png;base64,aaa",
|
|
1040
|
-
});
|
|
1041
|
-
await store.addcapture({
|
|
1042
|
-
id: "c2",
|
|
1043
|
-
runid: "run-1",
|
|
1044
|
-
stepid: "s2",
|
|
1045
|
-
kind: "shotfullpage",
|
|
1046
|
-
format: "png",
|
|
1047
|
-
width: 1280,
|
|
1048
|
-
height: 4000,
|
|
1049
|
-
capturedat: 2,
|
|
1050
|
-
bytes: "data:image/png;base64,bbb",
|
|
1051
|
-
});
|
|
1052
|
-
await store.addcapture({
|
|
1053
|
-
id: "c3",
|
|
1054
|
-
runid: "run-2",
|
|
1055
|
-
stepid: "s1",
|
|
1056
|
-
kind: "shotview",
|
|
1057
|
-
format: "webp",
|
|
1058
|
-
width: 640,
|
|
1059
|
-
height: 400,
|
|
1060
|
-
capturedat: 3,
|
|
1061
|
-
bytes: "data:image/webp;base64,ccc",
|
|
1062
|
-
});
|
|
1063
|
-
expect((await store.getcaptures()).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
|
|
1064
|
-
expect(await store.getcapture("c2")).toMatchObject({ kind: "shotfullpage", bytes: "data:image/png;base64,bbb" });
|
|
1065
|
-
expect(await store.getcapture("missing")).toBeUndefined();
|
|
1066
|
-
expect((await store.listcaptures({ runid: "run-1" })).map((record) => record.id)).toEqual(["c2", "c1"]);
|
|
1067
|
-
expect((await store.listcaptures({ stepid: "s1" })).map((record) => record.id)).toEqual(["c3", "c1"]);
|
|
1068
|
-
expect((await store.listcaptures({ runid: "run-1", kind: "shotview" })).map((record) => record.id)).toEqual(["c1"]);
|
|
1069
|
-
expect(await store.listcaptures({})).toHaveLength(3);
|
|
1070
|
-
await store.addpair({
|
|
1071
|
-
id: "p1",
|
|
1072
|
-
beforeid: "c1",
|
|
1073
|
-
afterid: "c2",
|
|
1074
|
-
actionkind: "click",
|
|
1075
|
-
target: "#submit",
|
|
1076
|
-
domsnapshotid: "7",
|
|
1077
|
-
at: 4,
|
|
1078
|
-
});
|
|
1079
|
-
await store.addpair({ id: "p2", beforeid: "c3", afterid: "c3", actionkind: "type", at: 5 });
|
|
1080
|
-
expect((await store.getpairs()).map((pair) => pair.id)).toEqual(["p2", "p1"]);
|
|
1081
|
-
expect((await store.getpairs("run-1")).map((pair) => pair.id)).toEqual(["p1"]);
|
|
1082
|
-
expect((await store.getpairs("run-2")).map((pair) => pair.id)).toEqual(["p2"]);
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
it("expires capture bytes after the configured retention window while keeping the metadata", async () => {
|
|
1086
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1087
|
-
await store.setsettings({ captureretention: 2 });
|
|
1088
|
-
await store.addcapture({
|
|
1089
|
-
id: "c1",
|
|
1090
|
-
runid: "run-1",
|
|
1091
|
-
stepid: "s1",
|
|
1092
|
-
kind: "shotview",
|
|
1093
|
-
format: "png",
|
|
1094
|
-
width: 10,
|
|
1095
|
-
height: 10,
|
|
1096
|
-
capturedat: 1,
|
|
1097
|
-
bytes: "data:1",
|
|
1098
|
-
});
|
|
1099
|
-
await store.addcapture({
|
|
1100
|
-
id: "c2",
|
|
1101
|
-
runid: "run-1",
|
|
1102
|
-
stepid: "s2",
|
|
1103
|
-
kind: "shotview",
|
|
1104
|
-
format: "png",
|
|
1105
|
-
width: 10,
|
|
1106
|
-
height: 10,
|
|
1107
|
-
capturedat: 2,
|
|
1108
|
-
bytes: "data:2",
|
|
1109
|
-
});
|
|
1110
|
-
await store.addcapture({
|
|
1111
|
-
id: "c3",
|
|
1112
|
-
runid: "run-1",
|
|
1113
|
-
stepid: "s3",
|
|
1114
|
-
kind: "shotview",
|
|
1115
|
-
format: "png",
|
|
1116
|
-
width: 10,
|
|
1117
|
-
height: 10,
|
|
1118
|
-
capturedat: 3,
|
|
1119
|
-
bytes: "data:3",
|
|
1120
|
-
});
|
|
1121
|
-
const stored = await store.getcaptures();
|
|
1122
|
-
expect(stored.map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
|
|
1123
|
-
expect(stored[0]).toMatchObject({ bytes: "data:3" });
|
|
1124
|
-
expect(stored[1]).toMatchObject({ bytes: "data:2" });
|
|
1125
|
-
expect(stored[2]).toMatchObject({ bytesexpired: true });
|
|
1126
|
-
expect(stored[2]?.bytes).toBeUndefined();
|
|
1127
|
-
expect((await store.listcaptures({ runid: "run-1" })).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
|
|
1128
|
-
const unbounded = new sessionmemory(new fakeadapter());
|
|
1129
|
-
await unbounded.addcapture({
|
|
1130
|
-
id: "c1",
|
|
1131
|
-
runid: "run-1",
|
|
1132
|
-
stepid: "s1",
|
|
1133
|
-
kind: "shotview",
|
|
1134
|
-
format: "png",
|
|
1135
|
-
width: 10,
|
|
1136
|
-
height: 10,
|
|
1137
|
-
capturedat: 1,
|
|
1138
|
-
bytes: "data:1",
|
|
1139
|
-
});
|
|
1140
|
-
await unbounded.addcapture({
|
|
1141
|
-
id: "c2",
|
|
1142
|
-
runid: "run-1",
|
|
1143
|
-
stepid: "s2",
|
|
1144
|
-
kind: "shotview",
|
|
1145
|
-
format: "png",
|
|
1146
|
-
width: 10,
|
|
1147
|
-
height: 10,
|
|
1148
|
-
capturedat: 2,
|
|
1149
|
-
bytes: "data:2",
|
|
1150
|
-
});
|
|
1151
|
-
expect((await unbounded.getcaptures()).every((record) => record.bytes !== undefined)).toBe(true);
|
|
1152
|
-
});
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
describe("media memory", () => {
|
|
1156
|
-
it("stores media records per run, filters by run and kind and returns recordings with their file reference", async () => {
|
|
1157
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1158
|
-
await store.addmedia({
|
|
1159
|
-
id: "pdf-1",
|
|
1160
|
-
runid: "run-1",
|
|
1161
|
-
stepid: "s1",
|
|
1162
|
-
pages: 3,
|
|
1163
|
-
pagewidth: 612,
|
|
1164
|
-
pageheight: 792,
|
|
1165
|
-
margins: { top: 28.8, right: 28.8, bottom: 28.8, left: 28.8 },
|
|
1166
|
-
landscape: false,
|
|
1167
|
-
scale: 1,
|
|
1168
|
-
bytes: 4200,
|
|
1169
|
-
at: 1,
|
|
1170
|
-
dataurl: "data:application/pdf;base64,aaa",
|
|
1171
|
-
name: "run-1-s1-1-capturepdf.pdf",
|
|
1172
|
-
});
|
|
1173
|
-
await store.addmedia({
|
|
1174
|
-
id: "rec-1",
|
|
1175
|
-
runid: "run-1",
|
|
1176
|
-
stepid: "s2",
|
|
1177
|
-
tabid: 4,
|
|
1178
|
-
kind: "screen",
|
|
1179
|
-
scope: "run",
|
|
1180
|
-
format: "frames",
|
|
1181
|
-
startedat: 2,
|
|
1182
|
-
endedat: 7,
|
|
1183
|
-
duration: 5,
|
|
1184
|
-
fps: 2,
|
|
1185
|
-
frames: ["f1", "f2"],
|
|
1186
|
-
file: "run-1-s2-2-recordscreen.json",
|
|
1187
|
-
bytes: 300,
|
|
1188
|
-
at: 2,
|
|
1189
|
-
});
|
|
1190
|
-
await store.addmedia({
|
|
1191
|
-
id: "frame-1",
|
|
1192
|
-
runid: "run-2",
|
|
1193
|
-
stepid: "s3",
|
|
1194
|
-
source: "video.tutorial",
|
|
1195
|
-
timestamp: 12.5,
|
|
1196
|
-
poster: true,
|
|
1197
|
-
format: "png",
|
|
1198
|
-
width: 1280,
|
|
1199
|
-
height: 720,
|
|
1200
|
-
at: 3,
|
|
1201
|
-
dataurl: "data:image/png;base64,bbb",
|
|
1202
|
-
});
|
|
1203
|
-
await store.addmedia({
|
|
1204
|
-
id: "canvas-1",
|
|
1205
|
-
runid: "run-2",
|
|
1206
|
-
stepid: "s4",
|
|
1207
|
-
element: "canvas.chart",
|
|
1208
|
-
context: "webgl",
|
|
1209
|
-
width: 600,
|
|
1210
|
-
height: 400,
|
|
1211
|
-
format: "png",
|
|
1212
|
-
at: 4,
|
|
1213
|
-
dataurl: "data:image/png;base64,ccc",
|
|
1214
|
-
});
|
|
1215
|
-
await store.addmedia({
|
|
1216
|
-
id: "stream-1",
|
|
1217
|
-
runid: "run-2",
|
|
1218
|
-
stepid: "s5",
|
|
1219
|
-
kind: "webrtc",
|
|
1220
|
-
tracks: 2,
|
|
1221
|
-
label: "stream-1",
|
|
1222
|
-
live: true,
|
|
1223
|
-
at: 5,
|
|
1224
|
-
});
|
|
1225
|
-
await store.addmedia({
|
|
1226
|
-
id: "asset-1",
|
|
1227
|
-
runid: "run-2",
|
|
1228
|
-
stepid: "s6",
|
|
1229
|
-
kind: "favicon",
|
|
1230
|
-
url: "https://example.com/icon.png",
|
|
1231
|
-
bytes: 800,
|
|
1232
|
-
sizes: "32x32",
|
|
1233
|
-
at: 6,
|
|
1234
|
-
});
|
|
1235
|
-
expect((await store.getmediarecords()).map((record) => record.id)).toEqual([
|
|
1236
|
-
"asset-1",
|
|
1237
|
-
"stream-1",
|
|
1238
|
-
"canvas-1",
|
|
1239
|
-
"frame-1",
|
|
1240
|
-
"rec-1",
|
|
1241
|
-
"pdf-1",
|
|
1242
|
-
]);
|
|
1243
|
-
expect((await store.listmedia({ runid: "run-1" })).map((record) => record.id)).toEqual(["rec-1", "pdf-1"]);
|
|
1244
|
-
expect((await store.listmedia({ kind: "recording" })).map((record) => record.id)).toEqual(["rec-1"]);
|
|
1245
|
-
expect((await store.listmedia({ kind: "pdf" })).map((record) => record.id)).toEqual(["pdf-1"]);
|
|
1246
|
-
expect((await store.listmedia({ runid: "run-2", kind: "frame" })).map((record) => record.id)).toEqual(["frame-1"]);
|
|
1247
|
-
expect(await store.listmedia({})).toHaveLength(6);
|
|
1248
|
-
expect(await store.getmediarecord("canvas-1")).toMatchObject({ context: "webgl" });
|
|
1249
|
-
expect(await store.getmediarecord("missing")).toBeUndefined();
|
|
1250
|
-
expect(await store.getrecording("rec-1")).toMatchObject({
|
|
1251
|
-
kind: "screen",
|
|
1252
|
-
file: "run-1-s2-2-recordscreen.json",
|
|
1253
|
-
frames: ["f1", "f2"],
|
|
1254
|
-
duration: 5,
|
|
1255
|
-
});
|
|
1256
|
-
expect(await store.getrecording("pdf-1")).toBeUndefined();
|
|
1257
|
-
await store.removemedia("stream-1");
|
|
1258
|
-
expect((await store.getmediarecords()).map((record) => record.id)).not.toContain("stream-1");
|
|
1259
|
-
});
|
|
1260
|
-
|
|
1261
|
-
it("expires media bytes after the retention window while keeping the recording index and metadata", async () => {
|
|
1262
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1263
|
-
await store.setsettings({ mediaretention: 2 });
|
|
1264
|
-
await store.addmedia({
|
|
1265
|
-
id: "pdf-1",
|
|
1266
|
-
runid: "run-1",
|
|
1267
|
-
stepid: "s1",
|
|
1268
|
-
pages: 1,
|
|
1269
|
-
pagewidth: 612,
|
|
1270
|
-
pageheight: 792,
|
|
1271
|
-
margins: { top: 28.8, right: 28.8, bottom: 28.8, left: 28.8 },
|
|
1272
|
-
landscape: false,
|
|
1273
|
-
scale: 1,
|
|
1274
|
-
bytes: 100,
|
|
1275
|
-
at: 1,
|
|
1276
|
-
dataurl: "data:1",
|
|
1277
|
-
});
|
|
1278
|
-
await store.addmedia({
|
|
1279
|
-
id: "rec-1",
|
|
1280
|
-
runid: "run-1",
|
|
1281
|
-
stepid: "s2",
|
|
1282
|
-
tabid: 4,
|
|
1283
|
-
kind: "audio",
|
|
1284
|
-
scope: "tab",
|
|
1285
|
-
format: "evidence",
|
|
1286
|
-
startedat: 2,
|
|
1287
|
-
frames: ["e1"],
|
|
1288
|
-
file: "rec.json",
|
|
1289
|
-
bytes: 50,
|
|
1290
|
-
at: 2,
|
|
1291
|
-
});
|
|
1292
|
-
await store.addmedia({
|
|
1293
|
-
id: "frame-1",
|
|
1294
|
-
runid: "run-1",
|
|
1295
|
-
stepid: "s3",
|
|
1296
|
-
source: "video",
|
|
1297
|
-
timestamp: 0,
|
|
1298
|
-
poster: false,
|
|
1299
|
-
format: "png",
|
|
1300
|
-
width: 10,
|
|
1301
|
-
height: 10,
|
|
1302
|
-
at: 3,
|
|
1303
|
-
dataurl: "data:3",
|
|
1304
|
-
});
|
|
1305
|
-
const stored = await store.getmediarecords();
|
|
1306
|
-
expect(stored.map((record) => record.id)).toEqual(["frame-1", "rec-1", "pdf-1"]);
|
|
1307
|
-
expect(stored[0]).toMatchObject({ dataurl: "data:3" });
|
|
1308
|
-
expect(stored[1]).toMatchObject({ bytes: 50 });
|
|
1309
|
-
expect(stored[2]).toMatchObject({ bytesexpired: true });
|
|
1310
|
-
expect((stored[2] as { dataurl?: string }).dataurl).toBeUndefined();
|
|
1311
|
-
await store.addmedia({
|
|
1312
|
-
id: "frame-2",
|
|
1313
|
-
runid: "run-1",
|
|
1314
|
-
stepid: "s4",
|
|
1315
|
-
source: "video",
|
|
1316
|
-
timestamp: 1,
|
|
1317
|
-
poster: false,
|
|
1318
|
-
format: "png",
|
|
1319
|
-
width: 10,
|
|
1320
|
-
height: 10,
|
|
1321
|
-
at: 4,
|
|
1322
|
-
dataurl: "data:4",
|
|
1323
|
-
});
|
|
1324
|
-
const after = await store.getmediarecords();
|
|
1325
|
-
expect(after.map((record) => record.id)).toEqual(["frame-2", "frame-1", "rec-1", "pdf-1"]);
|
|
1326
|
-
expect((after[2] as recordinglike).bytes).toBeUndefined();
|
|
1327
|
-
expect((after[2] as recordinglike).bytesexpired).toBe(true);
|
|
1328
|
-
expect((after[2] as recordinglike).frames).toEqual(["e1"]);
|
|
1329
|
-
expect((after[2] as recordinglike).file).toBe("rec.json");
|
|
1330
|
-
const unbounded = new sessionmemory(new fakeadapter());
|
|
1331
|
-
await unbounded.addmedia({
|
|
1332
|
-
id: "pdf-1",
|
|
1333
|
-
runid: "run-1",
|
|
1334
|
-
stepid: "s1",
|
|
1335
|
-
pages: 1,
|
|
1336
|
-
pagewidth: 612,
|
|
1337
|
-
pageheight: 792,
|
|
1338
|
-
margins: { top: 1, right: 1, bottom: 1, left: 1 },
|
|
1339
|
-
landscape: false,
|
|
1340
|
-
scale: 1,
|
|
1341
|
-
bytes: 100,
|
|
1342
|
-
at: 1,
|
|
1343
|
-
dataurl: "data:1",
|
|
1344
|
-
});
|
|
1345
|
-
await unbounded.addmedia({
|
|
1346
|
-
id: "rec-1",
|
|
1347
|
-
runid: "run-1",
|
|
1348
|
-
stepid: "s2",
|
|
1349
|
-
tabid: 4,
|
|
1350
|
-
kind: "screen",
|
|
1351
|
-
scope: "tab",
|
|
1352
|
-
format: "frames",
|
|
1353
|
-
startedat: 2,
|
|
1354
|
-
bytes: 50,
|
|
1355
|
-
at: 2,
|
|
1356
|
-
});
|
|
1357
|
-
expect(
|
|
1358
|
-
(await unbounded.getmediarecords()).every(
|
|
1359
|
-
(record) => (record as { bytesexpired?: boolean }).bytesexpired === undefined,
|
|
1360
|
-
),
|
|
1361
|
-
).toBe(true);
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
it("stores image batches with match counts and recording consent decisions per origin", async () => {
|
|
1365
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1366
|
-
await store.addimagebatch({
|
|
1367
|
-
id: "batch-1",
|
|
1368
|
-
runid: "run-1",
|
|
1369
|
-
stepid: "s1",
|
|
1370
|
-
images: [
|
|
1371
|
-
{ url: "https://example.com/a.png", alt: "Chart", width: 400, height: 300, bytes: 1200, mime: "image/png" },
|
|
1372
|
-
],
|
|
1373
|
-
matched: 1,
|
|
1374
|
-
downloaded: 1,
|
|
1375
|
-
at: 1,
|
|
1376
|
-
});
|
|
1377
|
-
await store.addimagebatch({
|
|
1378
|
-
id: "batch-2",
|
|
1379
|
-
runid: "run-1",
|
|
1380
|
-
stepid: "s2",
|
|
1381
|
-
images: [],
|
|
1382
|
-
matched: 0,
|
|
1383
|
-
downloaded: 0,
|
|
1384
|
-
at: 2,
|
|
1385
|
-
});
|
|
1386
|
-
await store.addimagebatch({
|
|
1387
|
-
id: "batch-1",
|
|
1388
|
-
runid: "run-1",
|
|
1389
|
-
stepid: "s1",
|
|
1390
|
-
images: [{ url: "https://example.com/b.png", alt: "", width: 50, height: 50, bytes: 10, mime: "image/png" }],
|
|
1391
|
-
matched: 1,
|
|
1392
|
-
downloaded: 0,
|
|
1393
|
-
at: 3,
|
|
1394
|
-
});
|
|
1395
|
-
expect((await store.getimagebatches()).map((batch) => batch.id)).toEqual(["batch-1", "batch-2"]);
|
|
1396
|
-
expect((await store.getimagebatches())[0]).toMatchObject({
|
|
1397
|
-
matched: 1,
|
|
1398
|
-
downloaded: 0,
|
|
1399
|
-
images: [{ url: "https://example.com/b.png" }],
|
|
1400
|
-
});
|
|
1401
|
-
await store.setrecordingconsent({
|
|
1402
|
-
id: "consent-1",
|
|
1403
|
-
prompt: "Record the checkout flow.",
|
|
1404
|
-
origin: "https://example.com",
|
|
1405
|
-
stepid: "s3",
|
|
1406
|
-
at: 4,
|
|
1407
|
-
});
|
|
1408
|
-
await store.setrecordingconsent({
|
|
1409
|
-
id: "consent-2",
|
|
1410
|
-
prompt: "Record the audio.",
|
|
1411
|
-
origin: "https://other.example",
|
|
1412
|
-
stepid: "s4",
|
|
1413
|
-
at: 5,
|
|
1414
|
-
});
|
|
1415
|
-
await store.setrecordingconsent({
|
|
1416
|
-
id: "consent-1",
|
|
1417
|
-
prompt: "Record the checkout flow.",
|
|
1418
|
-
origin: "https://example.com",
|
|
1419
|
-
stepid: "s3",
|
|
1420
|
-
approved: true,
|
|
1421
|
-
at: 4,
|
|
1422
|
-
});
|
|
1423
|
-
const consents = await store.getrecordingconsents();
|
|
1424
|
-
expect(consents.map((consent) => consent.id)).toEqual(["consent-1", "consent-2"]);
|
|
1425
|
-
expect(consents[0]).toMatchObject({ approved: true, origin: "https://example.com" });
|
|
1426
|
-
});
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
type recordinglike = { bytes?: number; bytesexpired?: boolean; frames?: string[]; file?: string };
|
|
1430
|
-
|
|
1431
|
-
describe("network observation memory", () => {
|
|
1432
|
-
const call = (id: string, origin: string, body?: string): callrecord => ({
|
|
1433
|
-
id,
|
|
1434
|
-
runid: "run-1",
|
|
1435
|
-
stepid: "f1",
|
|
1436
|
-
kind: "fetch",
|
|
1437
|
-
url: `${origin}/data`,
|
|
1438
|
-
origin,
|
|
1439
|
-
method: "GET",
|
|
1440
|
-
status: 200,
|
|
1441
|
-
statusclass: "success",
|
|
1442
|
-
duration: 10,
|
|
1443
|
-
retries: 0,
|
|
1444
|
-
bytes: (body ?? "").length,
|
|
1445
|
-
headernames: ["x-request-id"],
|
|
1446
|
-
...(body !== undefined ? { body } : {}),
|
|
1447
|
-
at: 1,
|
|
1448
|
-
});
|
|
1449
|
-
|
|
1450
|
-
it("stores call records with bodies and lists them filtered by run and origin", async () => {
|
|
1451
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1452
|
-
await store.addcall(call("c1", "https://api.example", '{"a":1}'));
|
|
1453
|
-
await store.addcall(call("c2", "https://other.example", '{"b":2}'));
|
|
1454
|
-
await store.addcall({ ...call("c3", "https://api.example"), runid: "run-2" });
|
|
1455
|
-
expect((await store.getcalls()).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
|
|
1456
|
-
expect((await store.listcalls({ runid: "run-1" })).map((record) => record.id)).toEqual(["c2", "c1"]);
|
|
1457
|
-
expect((await store.listcalls({ origin: "https://api.example" })).map((record) => record.id)).toEqual(["c3", "c1"]);
|
|
1458
|
-
expect((await store.listcalls({})).map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
|
|
1459
|
-
expect((await store.getcall("c2"))?.body).toBe('{"b":2}');
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
it("expires call bodies after the configured retention window while the metadata survives", async () => {
|
|
1463
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1464
|
-
await store.setsettings({ callretention: 2 });
|
|
1465
|
-
await store.addcall(call("c1", "https://api.example", "one"));
|
|
1466
|
-
await store.addcall(call("c2", "https://api.example", "two"));
|
|
1467
|
-
await store.addcall(call("c3", "https://api.example", "three"));
|
|
1468
|
-
const records = await store.getcalls();
|
|
1469
|
-
expect(records.map((record) => record.id)).toEqual(["c3", "c2", "c1"]);
|
|
1470
|
-
expect(records[0]?.body).toBe("three");
|
|
1471
|
-
expect(records[1]?.body).toBe("two");
|
|
1472
|
-
expect(records[2]?.body).toBeUndefined();
|
|
1473
|
-
expect(records[2]?.bodyexpired).toBe(true);
|
|
1474
|
-
expect(records[2]?.statusclass).toBe("success");
|
|
1475
|
-
});
|
|
1476
|
-
|
|
1477
|
-
it("stores endpoint definitions with version history and returns the newest schema", async () => {
|
|
1478
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1479
|
-
const schema = { fields: [{ name: "owner", kind: "string" as const, required: true }] };
|
|
1480
|
-
await store.setendpoint({
|
|
1481
|
-
name: "issues",
|
|
1482
|
-
method: "GET",
|
|
1483
|
-
url: "https://api.example/issues",
|
|
1484
|
-
schema,
|
|
1485
|
-
version: 1,
|
|
1486
|
-
at: 1,
|
|
1487
|
-
});
|
|
1488
|
-
await store.setendpoint({
|
|
1489
|
-
name: "issues",
|
|
1490
|
-
method: "GET",
|
|
1491
|
-
url: "https://api.example/v2/issues",
|
|
1492
|
-
schema,
|
|
1493
|
-
version: 1,
|
|
1494
|
-
at: 2,
|
|
1495
|
-
});
|
|
1496
|
-
const newest = await store.getendpoint("issues");
|
|
1497
|
-
expect(newest?.url).toBe("https://api.example/v2/issues");
|
|
1498
|
-
expect(newest?.version).toBe(2);
|
|
1499
|
-
const endpoints = await store.getendpoints();
|
|
1500
|
-
expect(endpoints).toHaveLength(1);
|
|
1501
|
-
expect(endpoints[0]?.version).toBe(2);
|
|
1502
|
-
expect(await store.getendpoint("missing")).toBeUndefined();
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
it("stores fetch consents per origin and api key references without key material", async () => {
|
|
1506
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1507
|
-
await store.setfetchconsent({
|
|
1508
|
-
id: "consent-1",
|
|
1509
|
-
origin: "https://api.example",
|
|
1510
|
-
headers: [{ name: "x-api-key", value: "secret" }],
|
|
1511
|
-
approved: true,
|
|
1512
|
-
expiresat: 99,
|
|
1513
|
-
at: 1,
|
|
1514
|
-
});
|
|
1515
|
-
const consents = await store.getfetchconsents();
|
|
1516
|
-
expect(consents[0]?.headers[0]).toEqual({ name: "x-api-key", value: "secret" });
|
|
1517
|
-
await store.setapikey({
|
|
1518
|
-
name: "github",
|
|
1519
|
-
origins: ["https://api.example"],
|
|
1520
|
-
header: "authorization",
|
|
1521
|
-
storageid: "apikey-github",
|
|
1522
|
-
createdat: 1,
|
|
1523
|
-
});
|
|
1524
|
-
await store.setsecret("apikey-github", "ghp-token");
|
|
1525
|
-
expect((await store.getapikeys())[0]).toMatchObject({ name: "github", header: "authorization" });
|
|
1526
|
-
expect(await store.getsecret("apikey-github")).toBe("ghp-token");
|
|
1527
|
-
await store.removeapikey("github");
|
|
1528
|
-
expect(await store.getapikeys()).toHaveLength(0);
|
|
1529
|
-
expect(await store.getsecret("apikey-github")).toBeUndefined();
|
|
1530
|
-
});
|
|
1531
|
-
});
|
|
1532
|
-
|
|
1533
|
-
describe("network observation part two memory", () => {
|
|
1534
|
-
it("stores channel records with message counters and stream subscriptions", async () => {
|
|
1535
|
-
const memory = new sessionmemory(new fakeadapter());
|
|
1536
|
-
const channel = {
|
|
1537
|
-
id: "ch1",
|
|
1538
|
-
runid: "run",
|
|
1539
|
-
stepid: "s1",
|
|
1540
|
-
kind: "websocket" as const,
|
|
1541
|
-
url: "wss://api.example/live",
|
|
1542
|
-
origin: "https://api.example",
|
|
1543
|
-
state: "open" as const,
|
|
1544
|
-
openedat: 1,
|
|
1545
|
-
sent: 0,
|
|
1546
|
-
received: 0,
|
|
1547
|
-
reconnects: 0,
|
|
1548
|
-
};
|
|
1549
|
-
await memory.addchannel(channel);
|
|
1550
|
-
await memory.addchannel({ ...channel, sent: 2, received: 5 });
|
|
1551
|
-
expect((await memory.getchannel("ch1"))?.sent).toBe(2);
|
|
1552
|
-
expect(await memory.getchannels()).toHaveLength(1);
|
|
1553
|
-
const subscription = {
|
|
1554
|
-
id: "sub1",
|
|
1555
|
-
runid: "run",
|
|
1556
|
-
stepid: "s2",
|
|
1557
|
-
url: "https://api.example/stream",
|
|
1558
|
-
origin: "https://api.example",
|
|
1559
|
-
state: "open" as const,
|
|
1560
|
-
events: 0,
|
|
1561
|
-
names: [] as string[],
|
|
1562
|
-
cancel: { kind: "stop" as const, value: "done" },
|
|
1563
|
-
openedat: 1,
|
|
1564
|
-
};
|
|
1565
|
-
await memory.setsubscription(subscription);
|
|
1566
|
-
await memory.setsubscription({ ...subscription, events: 9, names: ["userjoin"], lasteventid: "41" });
|
|
1567
|
-
expect((await memory.getsubscriptions())[0]).toMatchObject({ events: 9, lasteventid: "41" });
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
it("queues message envelopes with sequence integrity and drains consumed matches", async () => {
|
|
1571
|
-
const memory = new sessionmemory(new fakeadapter());
|
|
1572
|
-
await memory.addmessage({ channelid: "ch1", stream: "orders", payload: '{"id":1}', sequence: 1, at: 1 });
|
|
1573
|
-
await memory.addmessage({ channelid: "ch1", stream: "orders", payload: '{"id":2}', sequence: 2, at: 2 });
|
|
1574
|
-
await memory.addmessage({ channelid: "ch1", stream: "ticks", payload: "{}", sequence: 3, at: 3 });
|
|
1575
|
-
expect((await memory.getmessages()).map((envelope) => envelope.sequence)).toEqual([1, 2, 3]);
|
|
1576
|
-
expect((await memory.getmessages("ch1", "orders")).map((envelope) => envelope.payload)).toEqual([
|
|
1577
|
-
'{"id":1}',
|
|
1578
|
-
'{"id":2}',
|
|
1579
|
-
]);
|
|
1580
|
-
await memory.addmessage({ channelid: "ch1", stream: "orders", payload: '{"id":1}', sequence: 1, at: 1 });
|
|
1581
|
-
expect(await memory.getmessages()).toHaveLength(3);
|
|
1582
|
-
await memory.drainmessages([
|
|
1583
|
-
{ channelid: "ch1", sequence: 1 },
|
|
1584
|
-
{ channelid: "ch1", sequence: 3 },
|
|
1585
|
-
]);
|
|
1586
|
-
expect((await memory.getmessages()).map((envelope) => envelope.sequence)).toEqual([2]);
|
|
1587
|
-
});
|
|
1588
|
-
|
|
1589
|
-
it("stores exchanges with correlation ids and filters by run, origin and status", async () => {
|
|
1590
|
-
const memory = new sessionmemory(new fakeadapter());
|
|
1591
|
-
const exchange = {
|
|
1592
|
-
id: "e1",
|
|
1593
|
-
runid: "run",
|
|
1594
|
-
stepid: "s1",
|
|
1595
|
-
correlationid: "run-1",
|
|
1596
|
-
url: "https://api.example/items",
|
|
1597
|
-
origin: "https://api.example",
|
|
1598
|
-
method: "GET",
|
|
1599
|
-
status: 200,
|
|
1600
|
-
statusclass: "success",
|
|
1601
|
-
source: "extension" as const,
|
|
1602
|
-
timing: 10,
|
|
1603
|
-
bytes: 20,
|
|
1604
|
-
at: 1,
|
|
1605
|
-
};
|
|
1606
|
-
await memory.addexchange(exchange);
|
|
1607
|
-
await memory.addexchange({
|
|
1608
|
-
...exchange,
|
|
1609
|
-
id: "e2",
|
|
1610
|
-
correlationid: "run-2",
|
|
1611
|
-
url: "https://api.example/gone",
|
|
1612
|
-
status: 0,
|
|
1613
|
-
statusclass: "unknown",
|
|
1614
|
-
errorclass: "networkerror",
|
|
1615
|
-
});
|
|
1616
|
-
await memory.addexchange({ ...exchange, id: "e3", correlationid: "run-3", runid: "other" });
|
|
1617
|
-
expect((await memory.getexchange("e2"))?.errorclass).toBe("networkerror");
|
|
1618
|
-
expect(await memory.listexchanges({ runid: "run" })).toHaveLength(2);
|
|
1619
|
-
expect(await memory.listexchanges({ runid: "run", origin: "https://api.example" })).toHaveLength(2);
|
|
1620
|
-
expect(await memory.listexchanges({ runid: "run", status: 200 })).toHaveLength(1);
|
|
1621
|
-
expect(await memory.listexchanges({ runid: "run", status: "failed" })).toHaveLength(1);
|
|
1622
|
-
});
|
|
1623
|
-
|
|
1624
|
-
it("expires captured bodies after the configured retention window while the metadata survives", async () => {
|
|
1625
|
-
const memory = new sessionmemory(new fakeadapter());
|
|
1626
|
-
const body = (ref: string, correlationid: string): bodyrecord => ({
|
|
1627
|
-
ref,
|
|
1628
|
-
runid: "run",
|
|
1629
|
-
correlationid,
|
|
1630
|
-
url: "https://api.example/items",
|
|
1631
|
-
mime: "application/json",
|
|
1632
|
-
bytes: 8,
|
|
1633
|
-
body: '{"id":1}',
|
|
1634
|
-
at: 1,
|
|
1635
|
-
});
|
|
1636
|
-
await memory.addbody(body("b1", "run-1"));
|
|
1637
|
-
await memory.addbody(body("b2", "run-2"));
|
|
1638
|
-
expect((await memory.getbody("b1"))?.body).toBe('{"id":1}');
|
|
1639
|
-
await memory.setsettings({ bodyretention: 2 });
|
|
1640
|
-
await memory.addbody(body("b3", "run-3"));
|
|
1641
|
-
expect((await memory.getbodies()).map((record) => record.ref)).toEqual(["b3", "b2", "b1"]);
|
|
1642
|
-
expect((await memory.getbodies())[2]?.bodyexpired).toBe(true);
|
|
1643
|
-
await memory.addbody(body("b4", "run-4"));
|
|
1644
|
-
const stored = await memory.getbodies();
|
|
1645
|
-
expect(stored.map((record) => record.ref)).toEqual(["b4", "b3", "b2", "b1"]);
|
|
1646
|
-
expect(stored[1]?.body).toBe('{"id":1}');
|
|
1647
|
-
expect(stored[1]?.bodyexpired).toBeUndefined();
|
|
1648
|
-
expect(stored[2]?.body).toBeUndefined();
|
|
1649
|
-
expect(stored[2]?.bodyexpired).toBe(true);
|
|
1650
|
-
expect(stored[2]?.bytes).toBe(8);
|
|
1651
|
-
expect(stored[2]?.mime).toBe("application/json");
|
|
1652
|
-
expect(stored[2]?.correlationid).toBe("run-2");
|
|
1653
|
-
});
|
|
1654
|
-
|
|
1655
|
-
it("stores the page api map per origin", async () => {
|
|
1656
|
-
const memory = new sessionmemory(new fakeadapter());
|
|
1657
|
-
const entry = {
|
|
1658
|
-
endpoint: "https://api.example/items",
|
|
1659
|
-
method: "GET",
|
|
1660
|
-
mime: "application/json",
|
|
1661
|
-
frequency: 3,
|
|
1662
|
-
payloadshape: ["id"],
|
|
1663
|
-
jsonshare: 1,
|
|
1664
|
-
stability: 1,
|
|
1665
|
-
origin: "https://api.example",
|
|
1666
|
-
correlationids: ["run-1"],
|
|
1667
|
-
};
|
|
1668
|
-
await memory.setapimap("https://api.example", [entry]);
|
|
1669
|
-
await memory.setapimap("https://api.example", [{ ...entry, frequency: 7 }]);
|
|
1670
|
-
await memory.setapimap("https://other.example", [
|
|
1671
|
-
{ ...entry, origin: "https://other.example", endpoint: "https://other.example/x" },
|
|
1672
|
-
]);
|
|
1673
|
-
const stored = await memory.getapimap();
|
|
1674
|
-
expect(stored).toHaveLength(2);
|
|
1675
|
-
expect(stored.find((item) => item.origin === "https://api.example")?.frequency).toBe(7);
|
|
1676
|
-
});
|
|
1677
|
-
});
|
|
1678
|
-
|
|
1679
|
-
describe("network control storage", () => {
|
|
1680
|
-
it("stores block, mock and rewrite rule sets per run with hit counters and reverts", async () => {
|
|
1681
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1682
|
-
await store.addblockrule({
|
|
1683
|
-
id: "b1",
|
|
1684
|
-
runid: "run1",
|
|
1685
|
-
stepid: "s1",
|
|
1686
|
-
urlpattern: "https://ads.example/*",
|
|
1687
|
-
hits: 2,
|
|
1688
|
-
registeredat: 1,
|
|
1689
|
-
});
|
|
1690
|
-
await store.addblockrule({
|
|
1691
|
-
id: "b1",
|
|
1692
|
-
runid: "run1",
|
|
1693
|
-
stepid: "s1",
|
|
1694
|
-
urlpattern: "https://ads.example/*",
|
|
1695
|
-
hits: 3,
|
|
1696
|
-
revertedat: 9,
|
|
1697
|
-
registeredat: 1,
|
|
1698
|
-
});
|
|
1699
|
-
expect((await store.getblockrules())[0]).toMatchObject({ id: "b1", hits: 3, revertedat: 9 });
|
|
1700
|
-
await store.addmockspec({
|
|
1701
|
-
id: "m1",
|
|
1702
|
-
runid: "run1",
|
|
1703
|
-
stepid: "s1",
|
|
1704
|
-
urlpattern: "https://api.example/status",
|
|
1705
|
-
status: 204,
|
|
1706
|
-
body: "fixture",
|
|
1707
|
-
reviewed: true,
|
|
1708
|
-
hits: 1,
|
|
1709
|
-
registeredat: 1,
|
|
1710
|
-
});
|
|
1711
|
-
expect((await store.getmockspecs())[0]?.body).toBe("fixture");
|
|
1712
|
-
await store.addheaderule({
|
|
1713
|
-
id: "h1",
|
|
1714
|
-
runid: "run1",
|
|
1715
|
-
stepid: "s1",
|
|
1716
|
-
urlpattern: "https://api.example/*",
|
|
1717
|
-
name: "accept",
|
|
1718
|
-
operation: "set",
|
|
1719
|
-
value: "json",
|
|
1720
|
-
hits: 4,
|
|
1721
|
-
registeredat: 1,
|
|
1722
|
-
});
|
|
1723
|
-
expect((await store.getheaderules())[0]).toMatchObject({ operation: "set", hits: 4 });
|
|
1724
|
-
});
|
|
1725
|
-
|
|
1726
|
-
it("stores cookie operations per domain with timestamps and no values", async () => {
|
|
1727
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1728
|
-
await store.addcookieop({
|
|
1729
|
-
id: "c1",
|
|
1730
|
-
runid: "run1",
|
|
1731
|
-
stepid: "s1",
|
|
1732
|
-
kind: "write",
|
|
1733
|
-
domain: "example.com",
|
|
1734
|
-
names: ["session"],
|
|
1735
|
-
at: 1,
|
|
1736
|
-
});
|
|
1737
|
-
await store.addcookieop({
|
|
1738
|
-
id: "c2",
|
|
1739
|
-
runid: "run1",
|
|
1740
|
-
stepid: "s2",
|
|
1741
|
-
kind: "clear",
|
|
1742
|
-
domain: "api.example.com",
|
|
1743
|
-
names: [],
|
|
1744
|
-
at: 2,
|
|
1745
|
-
});
|
|
1746
|
-
expect((await store.getcookieops()).map((op) => op.kind)).toEqual(["clear", "write"]);
|
|
1747
|
-
expect((await store.getcookieops("example.com")).map((op) => op.id)).toEqual(["c1"]);
|
|
1748
|
-
expect(JSON.stringify(await store.getcookieops())).not.toContain("value");
|
|
1749
|
-
});
|
|
1750
|
-
|
|
1751
|
-
it("stores token metadata per provider without token values and lists per provider", async () => {
|
|
1752
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1753
|
-
await store.addtoken({
|
|
1754
|
-
id: "t1",
|
|
1755
|
-
provider: "providerco",
|
|
1756
|
-
origin: "https://api.example",
|
|
1757
|
-
scopes: ["read"],
|
|
1758
|
-
accessstorageid: "token-t1-access",
|
|
1759
|
-
refreshstorageid: "token-t1-refresh",
|
|
1760
|
-
expiresat: 99,
|
|
1761
|
-
at: 1,
|
|
1762
|
-
});
|
|
1763
|
-
await store.addtoken({
|
|
1764
|
-
id: "t2",
|
|
1765
|
-
provider: "otherco",
|
|
1766
|
-
origin: "https://other.example",
|
|
1767
|
-
scopes: ["write"],
|
|
1768
|
-
accessstorageid: "token-t2-access",
|
|
1769
|
-
expiresat: 99,
|
|
1770
|
-
at: 2,
|
|
1771
|
-
});
|
|
1772
|
-
expect((await store.listtokens()).map((token) => token.provider)).toEqual(["otherco", "providerco"]);
|
|
1773
|
-
expect((await store.listtokens("providerco")).map((token) => token.id)).toEqual(["t1"]);
|
|
1774
|
-
await store.addtoken({
|
|
1775
|
-
id: "t1",
|
|
1776
|
-
provider: "providerco",
|
|
1777
|
-
origin: "https://api.example",
|
|
1778
|
-
scopes: ["read"],
|
|
1779
|
-
accessstorageid: "token-t1-access",
|
|
1780
|
-
refreshstorageid: "token-t1-refresh",
|
|
1781
|
-
expiresat: 99,
|
|
1782
|
-
refreshedat: 5,
|
|
1783
|
-
at: 1,
|
|
1784
|
-
});
|
|
1785
|
-
expect((await store.listtokens("providerco"))[0]?.refreshedat).toBe(5);
|
|
1786
|
-
});
|
|
1787
|
-
|
|
1788
|
-
it("stores api key entries with last use timestamps and proxy route history with apply and revert times", async () => {
|
|
1789
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1790
|
-
await store.setapikey({
|
|
1791
|
-
name: "k",
|
|
1792
|
-
origins: ["https://api.example"],
|
|
1793
|
-
header: "authorization",
|
|
1794
|
-
storageid: "apikey-k",
|
|
1795
|
-
createdat: 1,
|
|
1796
|
-
});
|
|
1797
|
-
expect((await store.getapikeys())[0]?.lastuse).toBeUndefined();
|
|
1798
|
-
await store.touchapikey("k", 42);
|
|
1799
|
-
expect((await store.getapikeys())[0]?.lastuse).toBe(42);
|
|
1800
|
-
await store.addproxyroute({
|
|
1801
|
-
id: "p1",
|
|
1802
|
-
runid: "run1",
|
|
1803
|
-
stepid: "s1",
|
|
1804
|
-
scheme: "socks5",
|
|
1805
|
-
host: "proxy.example",
|
|
1806
|
-
port: 1080,
|
|
1807
|
-
bypass: ["https://api.example"],
|
|
1808
|
-
appliedat: 1,
|
|
1809
|
-
});
|
|
1810
|
-
await store.addproxyroute({
|
|
1811
|
-
id: "p1",
|
|
1812
|
-
runid: "run1",
|
|
1813
|
-
stepid: "s1",
|
|
1814
|
-
scheme: "socks5",
|
|
1815
|
-
host: "proxy.example",
|
|
1816
|
-
port: 1080,
|
|
1817
|
-
bypass: ["https://api.example"],
|
|
1818
|
-
appliedat: 1,
|
|
1819
|
-
revertedat: 7,
|
|
1820
|
-
});
|
|
1821
|
-
expect((await store.getproxyroutes())[0]).toMatchObject({ appliedat: 1, revertedat: 7 });
|
|
1822
|
-
});
|
|
1823
|
-
|
|
1824
|
-
it("expires rate limit states at their reset windows", async () => {
|
|
1825
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1826
|
-
await store.setratelimit({ origin: "https://api.example", remaining: 3, limit: 10, resetat: 100, at: 1 });
|
|
1827
|
-
await store.setratelimit({ origin: "https://slow.example", remaining: 0, resetat: 1000, at: 1 });
|
|
1828
|
-
expect((await store.getratelimits(150)).map((read) => read.origin)).toEqual(["https://slow.example"]);
|
|
1829
|
-
expect(await store.getratelimits(2000)).toEqual([]);
|
|
1830
|
-
});
|
|
1831
|
-
});
|
|
1832
|
-
|
|
1833
|
-
describe("run timeline storage", () => {
|
|
1834
|
-
it("stores timeline entries filtered by run, level and step id", async () => {
|
|
1835
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1836
|
-
await store.addtimelineentry({
|
|
1837
|
-
id: "t1",
|
|
1838
|
-
runid: "run1",
|
|
1839
|
-
stepid: "s1",
|
|
1840
|
-
time: 3,
|
|
1841
|
-
level: "error",
|
|
1842
|
-
source: "error",
|
|
1843
|
-
message: "boom",
|
|
1844
|
-
});
|
|
1845
|
-
await store.addtimelineentry({
|
|
1846
|
-
id: "t2",
|
|
1847
|
-
runid: "run1",
|
|
1848
|
-
stepid: "s2",
|
|
1849
|
-
time: 4,
|
|
1850
|
-
level: "info",
|
|
1851
|
-
source: "console",
|
|
1852
|
-
message: "hello",
|
|
1853
|
-
});
|
|
1854
|
-
await store.addtimelineentry({
|
|
1855
|
-
id: "t3",
|
|
1856
|
-
runid: "run2",
|
|
1857
|
-
stepid: "s1",
|
|
1858
|
-
time: 5,
|
|
1859
|
-
level: "error",
|
|
1860
|
-
source: "network",
|
|
1861
|
-
message: "failed request",
|
|
1862
|
-
});
|
|
1863
|
-
expect((await store.gettimeline()).map((entry) => entry.id)).toEqual(["t3", "t2", "t1"]);
|
|
1864
|
-
expect((await store.listtimeline({ runid: "run1" })).map((entry) => entry.id)).toEqual(["t2", "t1"]);
|
|
1865
|
-
expect((await store.listtimeline({ level: "error" })).map((entry) => entry.id)).toEqual(["t3", "t1"]);
|
|
1866
|
-
expect((await store.listtimeline({ runid: "run1", stepid: "s2" })).map((entry) => entry.id)).toEqual(["t2"]);
|
|
1867
|
-
});
|
|
1868
|
-
|
|
1869
|
-
it("expires timeline entries into level count summaries under the retention window", async () => {
|
|
1870
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1871
|
-
await store.setsettings({ timelineretention: 2 });
|
|
1872
|
-
await store.addtimelineentry({
|
|
1873
|
-
id: "t1",
|
|
1874
|
-
runid: "run1",
|
|
1875
|
-
stepid: "s1",
|
|
1876
|
-
time: 1,
|
|
1877
|
-
level: "error",
|
|
1878
|
-
source: "error",
|
|
1879
|
-
message: "one",
|
|
1880
|
-
});
|
|
1881
|
-
await store.addtimelineentry({
|
|
1882
|
-
id: "t2",
|
|
1883
|
-
runid: "run1",
|
|
1884
|
-
stepid: "s1",
|
|
1885
|
-
time: 2,
|
|
1886
|
-
level: "warn",
|
|
1887
|
-
source: "console",
|
|
1888
|
-
message: "two",
|
|
1889
|
-
});
|
|
1890
|
-
await store.addtimelineentry({
|
|
1891
|
-
id: "t3",
|
|
1892
|
-
runid: "run1",
|
|
1893
|
-
stepid: "s1",
|
|
1894
|
-
time: 3,
|
|
1895
|
-
level: "error",
|
|
1896
|
-
source: "error",
|
|
1897
|
-
message: "three",
|
|
1898
|
-
});
|
|
1899
|
-
expect((await store.gettimeline()).map((entry) => entry.id)).toEqual(["t3", "t2"]);
|
|
1900
|
-
const summaries = await store.getlevelsummaries();
|
|
1901
|
-
expect(summaries).toHaveLength(1);
|
|
1902
|
-
expect(summaries[0]).toMatchObject({ runid: "run1", counts: { error: 1 } });
|
|
1903
|
-
});
|
|
1904
|
-
|
|
1905
|
-
it("stores error and rejection records with stack frames and long tasks with attributions", async () => {
|
|
1906
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1907
|
-
await store.adderrorrecord({
|
|
1908
|
-
id: "e1",
|
|
1909
|
-
runid: "run1",
|
|
1910
|
-
stepid: "s1",
|
|
1911
|
-
message: "boom",
|
|
1912
|
-
frames: [{ functionname: "load", url: "https://example.com/app.js", line: 42, column: 7 }],
|
|
1913
|
-
sourceurl: "https://example.com/app.js",
|
|
1914
|
-
line: 42,
|
|
1915
|
-
at: 1,
|
|
1916
|
-
});
|
|
1917
|
-
await store.addrejectionrecord({
|
|
1918
|
-
id: "r1",
|
|
1919
|
-
runid: "run1",
|
|
1920
|
-
stepid: "s1",
|
|
1921
|
-
reason: "TypeError: failed",
|
|
1922
|
-
frames: [],
|
|
1923
|
-
at: 2,
|
|
1924
|
-
});
|
|
1925
|
-
await store.addlongtask({
|
|
1926
|
-
id: "l1",
|
|
1927
|
-
runid: "run1",
|
|
1928
|
-
stepid: "s1",
|
|
1929
|
-
duration: 90,
|
|
1930
|
-
starttime: 10,
|
|
1931
|
-
attributions: ["same-origin"],
|
|
1932
|
-
at: 3,
|
|
1933
|
-
});
|
|
1934
|
-
expect((await store.geterrorrecords())[0]?.frames[0]).toMatchObject({ functionname: "load", line: 42 });
|
|
1935
|
-
expect((await store.getrejectionrecords())[0]?.reason).toBe("TypeError: failed");
|
|
1936
|
-
expect((await store.getlongtasks())[0]).toMatchObject({ duration: 90, attributions: ["same-origin"] });
|
|
1937
|
-
});
|
|
1938
|
-
|
|
1939
|
-
it("stores the console diff result, rotation targets and per origin console consents", async () => {
|
|
1940
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1941
|
-
await store.addconsolediff({
|
|
1942
|
-
base: "run1",
|
|
1943
|
-
target: "run2",
|
|
1944
|
-
lines: [{ kind: "added", text: "fresh" }],
|
|
1945
|
-
added: 1,
|
|
1946
|
-
removed: 0,
|
|
1947
|
-
repeated: 0,
|
|
1948
|
-
at: 9,
|
|
1949
|
-
});
|
|
1950
|
-
expect((await store.getdiff())?.base).toBe("run1");
|
|
1951
|
-
await store.addrotationtarget({ target: "overflow1", runid: "run1", entries: 12, at: 9 });
|
|
1952
|
-
await store.addrotationtarget({ target: "overflow1", runid: "run2", entries: 3, at: 10 });
|
|
1953
|
-
expect((await store.getrotationtargets()).map((record) => record.runid)).toEqual(["run2", "run1"]);
|
|
1954
|
-
await store.setconsoleconsent({
|
|
1955
|
-
id: "c1",
|
|
1956
|
-
prompt: "Console capture on https://example.com",
|
|
1957
|
-
origin: "https://example.com",
|
|
1958
|
-
stepid: "s1",
|
|
1959
|
-
approved: true,
|
|
1960
|
-
at: 1,
|
|
1961
|
-
});
|
|
1962
|
-
expect((await store.getconsoleconsents())[0]).toMatchObject({ origin: "https://example.com", approved: true });
|
|
1963
|
-
});
|
|
1964
|
-
});
|
|
1965
|
-
|
|
1966
|
-
describe("devtools session storage", () => {
|
|
1967
|
-
it("stores session records with domains and command outcomes with durations and error classes", async () => {
|
|
1968
|
-
const store = new sessionmemory(new fakeadapter());
|
|
1969
|
-
await store.setcdpsession({
|
|
1970
|
-
id: "cdp1",
|
|
1971
|
-
runid: "plan",
|
|
1972
|
-
stepid: "a1",
|
|
1973
|
-
tabid: 4,
|
|
1974
|
-
origin: "https://example.com",
|
|
1975
|
-
attachedat: 1,
|
|
1976
|
-
domains: ["Runtime", "Debugger"],
|
|
1977
|
-
debuggerversion: "devthink instrumented harness 1.1.46 (no chrome.debugger permission)",
|
|
1978
|
-
});
|
|
1979
|
-
await store.setcdpsession({
|
|
1980
|
-
id: "cdp2",
|
|
1981
|
-
runid: "plan",
|
|
1982
|
-
stepid: "a2",
|
|
1983
|
-
tabid: 4,
|
|
1984
|
-
origin: "https://example.com",
|
|
1985
|
-
attachedat: 2,
|
|
1986
|
-
domains: ["Log"],
|
|
1987
|
-
debuggerversion: "harness",
|
|
1988
|
-
detachedat: 30,
|
|
1989
|
-
});
|
|
1990
|
-
expect((await store.getcdpsessions()).map((session) => session.id)).toEqual(["cdp2", "cdp1"]);
|
|
1991
|
-
expect((await store.getcdpsessions())[0]?.domains).toEqual(["Log"]);
|
|
1992
|
-
await store.addcdpcommand({
|
|
1993
|
-
id: "cmd1",
|
|
1994
|
-
sessionid: "cdp1",
|
|
1995
|
-
runid: "plan",
|
|
1996
|
-
stepid: "c1",
|
|
1997
|
-
method: "Runtime.evaluate",
|
|
1998
|
-
domain: "Runtime",
|
|
1999
|
-
duration: 12,
|
|
2000
|
-
at: 3,
|
|
2001
|
-
});
|
|
2002
|
-
await store.addcdpcommand({
|
|
2003
|
-
id: "cmd2",
|
|
2004
|
-
sessionid: "cdp1",
|
|
2005
|
-
runid: "plan",
|
|
2006
|
-
stepid: "c2",
|
|
2007
|
-
method: "DOM.getSnapshot",
|
|
2008
|
-
domain: "DOM",
|
|
2009
|
-
duration: 4,
|
|
2010
|
-
errorclass: "uninstrumented",
|
|
2011
|
-
at: 4,
|
|
2012
|
-
});
|
|
2013
|
-
const commands = await store.getcdpcommands();
|
|
2014
|
-
expect(commands[0]).toMatchObject({ method: "DOM.getSnapshot", errorclass: "uninstrumented" });
|
|
2015
|
-
expect(commands[1]).toMatchObject({ method: "Runtime.evaluate", duration: 12 });
|
|
2016
|
-
});
|
|
2017
|
-
|
|
2018
|
-
it("stores event rules, breakpoints with hit counts, watch expressions and overrides with provenance", async () => {
|
|
2019
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2020
|
-
await store.setcdpeventrule({
|
|
2021
|
-
id: "r1",
|
|
2022
|
-
sessionid: "cdp1",
|
|
2023
|
-
runid: "plan",
|
|
2024
|
-
stepid: "w1",
|
|
2025
|
-
domain: "Log",
|
|
2026
|
-
event: "entryAdded",
|
|
2027
|
-
match: "fixture",
|
|
2028
|
-
events: 4,
|
|
2029
|
-
registeredat: 1,
|
|
2030
|
-
});
|
|
2031
|
-
await store.setcdpeventrule({
|
|
2032
|
-
id: "r1",
|
|
2033
|
-
sessionid: "cdp1",
|
|
2034
|
-
runid: "plan",
|
|
2035
|
-
stepid: "w1",
|
|
2036
|
-
domain: "Log",
|
|
2037
|
-
event: "entryAdded",
|
|
2038
|
-
match: "fixture",
|
|
2039
|
-
events: 6,
|
|
2040
|
-
registeredat: 1,
|
|
2041
|
-
closedat: 9,
|
|
2042
|
-
});
|
|
2043
|
-
expect(await store.getcdpeventrules()).toHaveLength(1);
|
|
2044
|
-
expect((await store.getcdpeventrules())[0]).toMatchObject({ events: 6, closedat: 9 });
|
|
2045
|
-
await store.addbreakpoint({
|
|
2046
|
-
id: "bp1",
|
|
2047
|
-
runid: "plan",
|
|
2048
|
-
stepid: "b1",
|
|
2049
|
-
url: "https://example.com/app.js",
|
|
2050
|
-
line: 5,
|
|
2051
|
-
condition: "items.length > 0",
|
|
2052
|
-
hits: 1,
|
|
2053
|
-
registeredat: 1,
|
|
2054
|
-
});
|
|
2055
|
-
await store.addbreakpoint({
|
|
2056
|
-
id: "bp1",
|
|
2057
|
-
runid: "plan",
|
|
2058
|
-
stepid: "b1",
|
|
2059
|
-
url: "https://example.com/app.js",
|
|
2060
|
-
line: 5,
|
|
2061
|
-
condition: "items.length > 0",
|
|
2062
|
-
hits: 4,
|
|
2063
|
-
registeredat: 1,
|
|
2064
|
-
revertedat: 8,
|
|
2065
|
-
});
|
|
2066
|
-
expect(await store.getbreakpoints()).toHaveLength(1);
|
|
2067
|
-
expect((await store.getbreakpoints())[0]).toMatchObject({ hits: 4, revertedat: 8 });
|
|
2068
|
-
await store.setwatchexpression({
|
|
2069
|
-
id: "we1",
|
|
2070
|
-
runid: "plan",
|
|
2071
|
-
stepid: "w1",
|
|
2072
|
-
expression: "items.length",
|
|
2073
|
-
scope: "topframe",
|
|
2074
|
-
reviewed: true,
|
|
2075
|
-
values: [{ pauseid: "p1", value: "3", at: 4 }],
|
|
2076
|
-
at: 2,
|
|
2077
|
-
});
|
|
2078
|
-
await store.setwatchexpression({
|
|
2079
|
-
id: "we1",
|
|
2080
|
-
runid: "plan",
|
|
2081
|
-
stepid: "w1",
|
|
2082
|
-
expression: "items.length",
|
|
2083
|
-
scope: "topframe",
|
|
2084
|
-
reviewed: true,
|
|
2085
|
-
values: [
|
|
2086
|
-
{ pauseid: "p1", value: "3", at: 4 },
|
|
2087
|
-
{ pauseid: "p2", value: "8", at: 6 },
|
|
2088
|
-
],
|
|
2089
|
-
at: 2,
|
|
2090
|
-
});
|
|
2091
|
-
expect((await store.getwatchexpressions())[0]?.values).toHaveLength(2);
|
|
2092
|
-
await store.addscriptoverride({
|
|
2093
|
-
id: "ov1",
|
|
2094
|
-
runid: "plan",
|
|
2095
|
-
stepid: "o1",
|
|
2096
|
-
urlpattern: "https://cdn.example/vendor.js",
|
|
2097
|
-
source: "window.fixture = true;",
|
|
2098
|
-
reviewed: true,
|
|
2099
|
-
reviewedat: 3,
|
|
2100
|
-
hits: 2,
|
|
2101
|
-
appliedat: 3,
|
|
2102
|
-
});
|
|
2103
|
-
expect((await store.getscriptoverrides())[0]).toMatchObject({ reviewed: true, hits: 2 });
|
|
2104
|
-
});
|
|
2105
|
-
|
|
2106
|
-
it("stores pause states per run and expires the call frames under the retention window", async () => {
|
|
2107
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2108
|
-
await store.addpause({
|
|
2109
|
-
id: "p1",
|
|
2110
|
-
runid: "plan",
|
|
2111
|
-
stepid: "b1",
|
|
2112
|
-
reason: "breakpoint",
|
|
2113
|
-
callframes: [{ url: "https://example.com/app.js", line: 5 }],
|
|
2114
|
-
hitbreakpoint: "bp1",
|
|
2115
|
-
domsnapshotid: "dom-1",
|
|
2116
|
-
at: 1,
|
|
2117
|
-
});
|
|
2118
|
-
await store.addpause({
|
|
2119
|
-
id: "p2",
|
|
2120
|
-
runid: "plan",
|
|
2121
|
-
stepid: "b1",
|
|
2122
|
-
reason: "step",
|
|
2123
|
-
callframes: [{ url: "https://example.com/app.js", line: 6 }],
|
|
2124
|
-
at: 2,
|
|
2125
|
-
});
|
|
2126
|
-
await store.addpause({ id: "p3", runid: "other", stepid: "b1", reason: "breakpoint", callframes: [], at: 3 });
|
|
2127
|
-
expect((await store.listpauses("plan")).map((pause) => pause.id)).toEqual(["p2", "p1"]);
|
|
2128
|
-
expect((await store.listpauses("other")).map((pause) => pause.id)).toEqual(["p3"]);
|
|
2129
|
-
const retained = new sessionmemory(new fakeadapter());
|
|
2130
|
-
await retained.setsettings({ pauseretention: 2 });
|
|
2131
|
-
await retained.addpause({
|
|
2132
|
-
id: "p1",
|
|
2133
|
-
runid: "plan",
|
|
2134
|
-
stepid: "b1",
|
|
2135
|
-
reason: "breakpoint",
|
|
2136
|
-
callframes: [{ url: "https://example.com/app.js", line: 5 }],
|
|
2137
|
-
hitbreakpoint: "bp1",
|
|
2138
|
-
domsnapshotid: "dom-1",
|
|
2139
|
-
at: 1,
|
|
2140
|
-
});
|
|
2141
|
-
await retained.addpause({
|
|
2142
|
-
id: "p2",
|
|
2143
|
-
runid: "plan",
|
|
2144
|
-
stepid: "b1",
|
|
2145
|
-
reason: "step",
|
|
2146
|
-
callframes: [{ url: "https://example.com/app.js", line: 6 }],
|
|
2147
|
-
at: 2,
|
|
2148
|
-
});
|
|
2149
|
-
await retained.addpause({
|
|
2150
|
-
id: "p3",
|
|
2151
|
-
runid: "plan",
|
|
2152
|
-
stepid: "b1",
|
|
2153
|
-
reason: "step",
|
|
2154
|
-
callframes: [{ url: "https://example.com/app.js", line: 7 }],
|
|
2155
|
-
at: 3,
|
|
2156
|
-
});
|
|
2157
|
-
const pauses = await retained.getpauses();
|
|
2158
|
-
expect(pauses).toHaveLength(3);
|
|
2159
|
-
const expired = pauses.find((pause) => pause.id === "p1");
|
|
2160
|
-
if (!expired) throw new Error("The expired pause must stay stored.");
|
|
2161
|
-
expect(expired.framesexpired).toBe(true);
|
|
2162
|
-
expect(expired.callframes).toEqual([]);
|
|
2163
|
-
expect(expired.domsnapshotid).toBeUndefined();
|
|
2164
|
-
expect(expired.reason).toBe("breakpoint");
|
|
2165
|
-
expect(expired.hitbreakpoint).toBe("bp1");
|
|
2166
|
-
const kept = pauses.find((pause) => pause.id === "p3");
|
|
2167
|
-
expect(kept?.framesexpired).toBeUndefined();
|
|
2168
|
-
expect(kept?.callframes).toHaveLength(1);
|
|
2169
|
-
});
|
|
2170
|
-
|
|
2171
|
-
it("stores debugger consents per origin and revokes every approved record of one origin", async () => {
|
|
2172
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2173
|
-
await store.setdebuggergrant({
|
|
2174
|
-
id: "g1",
|
|
2175
|
-
prompt: "Debugger attach on https://example.com",
|
|
2176
|
-
origin: "https://example.com",
|
|
2177
|
-
domains: ["Runtime"],
|
|
2178
|
-
approved: true,
|
|
2179
|
-
consentedat: 1,
|
|
2180
|
-
});
|
|
2181
|
-
await store.setdebuggergrant({
|
|
2182
|
-
id: "g2",
|
|
2183
|
-
prompt: "Debugger attach on https://api.example",
|
|
2184
|
-
origin: "https://api.example",
|
|
2185
|
-
domains: ["Network"],
|
|
2186
|
-
approved: true,
|
|
2187
|
-
consentedat: 2,
|
|
2188
|
-
});
|
|
2189
|
-
expect(await store.getdebuggergrants()).toHaveLength(2);
|
|
2190
|
-
const revoked = await store.revokedebuggergrants("https://example.com", 9);
|
|
2191
|
-
expect(revoked).toBe(1);
|
|
2192
|
-
const grants = await store.getdebuggergrants();
|
|
2193
|
-
expect(grants.find((grant) => grant.id === "g1")?.revokedat).toBe(9);
|
|
2194
|
-
expect(grants.find((grant) => grant.id === "g2")?.revokedat).toBeUndefined();
|
|
2195
|
-
expect(await store.revokedebuggergrants("https://example.com", 10)).toBe(0);
|
|
2196
|
-
});
|
|
2197
|
-
});
|
|
2198
|
-
|
|
2199
|
-
describe("profiler storage", () => {
|
|
2200
|
-
const now = 1_800_000_000_000;
|
|
2201
|
-
|
|
2202
|
-
it("stores flow metric series per run and heap records with byte and node counts", async () => {
|
|
2203
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2204
|
-
await store.addflowmetric({
|
|
2205
|
-
id: "f1",
|
|
2206
|
-
runid: "plan",
|
|
2207
|
-
stepid: "m1",
|
|
2208
|
-
name: "navigation",
|
|
2209
|
-
start: 0,
|
|
2210
|
-
end: 800,
|
|
2211
|
-
duration: 800,
|
|
2212
|
-
steps: ["s1"],
|
|
2213
|
-
at: now,
|
|
2214
|
-
});
|
|
2215
|
-
await store.addflowmetric({
|
|
2216
|
-
id: "f2",
|
|
2217
|
-
runid: "plan",
|
|
2218
|
-
stepid: "m1",
|
|
2219
|
-
name: "blocking",
|
|
2220
|
-
start: 0,
|
|
2221
|
-
end: 100,
|
|
2222
|
-
duration: 70,
|
|
2223
|
-
steps: ["s1"],
|
|
2224
|
-
at: now,
|
|
2225
|
-
});
|
|
2226
|
-
await store.addflowmetric({
|
|
2227
|
-
id: "f3",
|
|
2228
|
-
runid: "other",
|
|
2229
|
-
stepid: "m1",
|
|
2230
|
-
name: "navigation",
|
|
2231
|
-
start: 0,
|
|
2232
|
-
end: 10,
|
|
2233
|
-
duration: 10,
|
|
2234
|
-
steps: [],
|
|
2235
|
-
at: now,
|
|
2236
|
-
});
|
|
2237
|
-
expect((await store.listflowmetrics("plan")).map((metric) => metric.name)).toEqual(["blocking", "navigation"]);
|
|
2238
|
-
expect(await store.getflowmetrics()).toHaveLength(3);
|
|
2239
|
-
await store.setheaprecord({
|
|
2240
|
-
id: "h1",
|
|
2241
|
-
runid: "plan",
|
|
2242
|
-
stepid: "hs",
|
|
2243
|
-
origin: "https://example.com",
|
|
2244
|
-
bytesize: 12_000_000,
|
|
2245
|
-
nodecount: 1450,
|
|
2246
|
-
capturedat: now,
|
|
2247
|
-
});
|
|
2248
|
-
expect((await store.getheaprecords())[0]).toMatchObject({ bytesize: 12_000_000, nodecount: 1450 });
|
|
2249
|
-
expect((await store.getheaprecords())[0]?.bytesexpired).toBeUndefined();
|
|
2250
|
-
});
|
|
2251
|
-
|
|
2252
|
-
it("stores growth samples with computed trends and cpu profiles with hot functions", async () => {
|
|
2253
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2254
|
-
await store.addgrowsample({
|
|
2255
|
-
id: "g1",
|
|
2256
|
-
runid: "plan",
|
|
2257
|
-
stepid: "s1",
|
|
2258
|
-
usedbytes: 1_000_000,
|
|
2259
|
-
limitbytes: 4_000_000,
|
|
2260
|
-
at: now,
|
|
2261
|
-
});
|
|
2262
|
-
await store.addgrowsample({
|
|
2263
|
-
id: "g2",
|
|
2264
|
-
runid: "plan",
|
|
2265
|
-
stepid: "s2",
|
|
2266
|
-
usedbytes: 3_000_000,
|
|
2267
|
-
limitbytes: 4_000_000,
|
|
2268
|
-
at: now + 1000,
|
|
2269
|
-
});
|
|
2270
|
-
expect(await store.listgrowsamples("plan")).toHaveLength(2);
|
|
2271
|
-
await store.settrend({ runid: "plan", slope: 2000, samples: 2, flaggedsteps: ["s2"], at: now + 2000 });
|
|
2272
|
-
await store.settrend({ runid: "plan", slope: 2000, samples: 2, flaggedsteps: ["s2", "s3"], at: now + 3000 });
|
|
2273
|
-
expect(await store.gettrends()).toHaveLength(1);
|
|
2274
|
-
expect((await store.gettrends())[0]?.flaggedsteps).toEqual(["s2", "s3"]);
|
|
2275
|
-
await store.setcpuprofile({
|
|
2276
|
-
id: "c1",
|
|
2277
|
-
runid: "plan",
|
|
2278
|
-
stepid: "cpu",
|
|
2279
|
-
origin: "https://example.com",
|
|
2280
|
-
duration: 500,
|
|
2281
|
-
samplecount: 3,
|
|
2282
|
-
hotfunctions: ["render", "parse"],
|
|
2283
|
-
at: now,
|
|
2284
|
-
});
|
|
2285
|
-
expect((await store.getcpuprofiles())[0]).toMatchObject({
|
|
2286
|
-
duration: 500,
|
|
2287
|
-
samplecount: 3,
|
|
2288
|
-
hotfunctions: ["render", "parse"],
|
|
2289
|
-
});
|
|
2290
|
-
});
|
|
2291
|
-
|
|
2292
|
-
it("stores shift entries, traces with annotations and source map references with per origin consents", async () => {
|
|
2293
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2294
|
-
await store.addshiftentry({
|
|
2295
|
-
id: "sh1",
|
|
2296
|
-
runid: "plan",
|
|
2297
|
-
stepid: "ws",
|
|
2298
|
-
score: 0.12,
|
|
2299
|
-
starttime: 40,
|
|
2300
|
-
selectors: ["img#hero"],
|
|
2301
|
-
at: now,
|
|
2302
|
-
});
|
|
2303
|
-
expect((await store.getshiftentries())[0]).toMatchObject({ score: 0.12, selectors: ["img#hero"] });
|
|
2304
|
-
await store.settracerecord({
|
|
2305
|
-
id: "t1",
|
|
2306
|
-
runid: "plan",
|
|
2307
|
-
stepid: "tl",
|
|
2308
|
-
origin: "https://example.com",
|
|
2309
|
-
categories: ["scripting", "network"],
|
|
2310
|
-
bytesize: 400,
|
|
2311
|
-
events: 2,
|
|
2312
|
-
annotations: [{ stepid: "s1", label: "open", offset: 100 }],
|
|
2313
|
-
startedat: now,
|
|
2314
|
-
endedat: now + 500,
|
|
2315
|
-
});
|
|
2316
|
-
await store.settracerecord({
|
|
2317
|
-
id: "t2",
|
|
2318
|
-
runid: "other",
|
|
2319
|
-
stepid: "tl",
|
|
2320
|
-
origin: "https://example.com",
|
|
2321
|
-
categories: ["scripting"],
|
|
2322
|
-
bytesize: 40,
|
|
2323
|
-
events: 1,
|
|
2324
|
-
annotations: [],
|
|
2325
|
-
startedat: now,
|
|
2326
|
-
endedat: now + 500,
|
|
2327
|
-
});
|
|
2328
|
-
expect((await store.listtraces({ runid: "plan" })).map((trace) => trace.id)).toEqual(["t1"]);
|
|
2329
|
-
expect((await store.listtraces({ categories: ["scripting"] })).map((trace) => trace.id)).toEqual(["t2", "t1"]);
|
|
2330
|
-
expect(
|
|
2331
|
-
(await store.listtraces({ runid: "plan", categories: ["scripting", "network"] })).map((trace) => trace.id),
|
|
2332
|
-
).toEqual(["t1"]);
|
|
2333
|
-
await store.settracefile("t1", '{"devthinktrace":"1.1.47"}');
|
|
2334
|
-
expect(await store.gettracefile("t1")).toBe('{"devthinktrace":"1.1.47"}');
|
|
2335
|
-
expect(await store.gettracefile("t2")).toBeUndefined();
|
|
2336
|
-
await store.setsourcemapref({
|
|
2337
|
-
id: "sm1",
|
|
2338
|
-
runid: "plan",
|
|
2339
|
-
stepid: "sm",
|
|
2340
|
-
origin: "https://example.com",
|
|
2341
|
-
scripturl: "https://example.com/app.js",
|
|
2342
|
-
mapurl: "https://example.com/app.js.map",
|
|
2343
|
-
parsed: true,
|
|
2344
|
-
at: now,
|
|
2345
|
-
});
|
|
2346
|
-
expect((await store.getsourcemaps())[0]).toMatchObject({ parsed: true });
|
|
2347
|
-
await store.setsourcemapconsent({
|
|
2348
|
-
id: "c1",
|
|
2349
|
-
prompt: "Source map capture prompt",
|
|
2350
|
-
origin: "https://example.com",
|
|
2351
|
-
consentedat: now,
|
|
2352
|
-
});
|
|
2353
|
-
await store.setsourcemapconsent({
|
|
2354
|
-
id: "c1",
|
|
2355
|
-
prompt: "Source map capture prompt",
|
|
2356
|
-
origin: "https://example.com",
|
|
2357
|
-
approved: true,
|
|
2358
|
-
consentedat: now,
|
|
2359
|
-
usedat: now + 1,
|
|
2360
|
-
});
|
|
2361
|
-
expect((await store.getsourcemapconsents())[0]?.approved).toBe(true);
|
|
2362
|
-
expect(await store.revokesourcemapconsents("https://example.com", now + 2)).toBe(1);
|
|
2363
|
-
expect((await store.getsourcemapconsents())[0]?.revokedat).toBe(now + 2);
|
|
2364
|
-
});
|
|
2365
|
-
|
|
2366
|
-
it("expires the heavy profile bytes after the configured retention window while the metadata survives", async () => {
|
|
2367
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2368
|
-
await store.setsettings({ profileretention: 1000 });
|
|
2369
|
-
const stale = Date.now() - 2000;
|
|
2370
|
-
await store.setheaprecord({
|
|
2371
|
-
id: "h1",
|
|
2372
|
-
runid: "plan",
|
|
2373
|
-
stepid: "hs",
|
|
2374
|
-
origin: "https://example.com",
|
|
2375
|
-
bytesize: 12_000_000,
|
|
2376
|
-
nodecount: 1450,
|
|
2377
|
-
capturedat: stale,
|
|
2378
|
-
});
|
|
2379
|
-
expect((await store.getheaprecords())[0]?.bytesexpired).toBe(true);
|
|
2380
|
-
expect((await store.getheaprecords())[0]).toMatchObject({ bytesize: 12_000_000, nodecount: 1450 });
|
|
2381
|
-
await store.setcpuprofile({
|
|
2382
|
-
id: "c1",
|
|
2383
|
-
runid: "plan",
|
|
2384
|
-
stepid: "cpu",
|
|
2385
|
-
origin: "https://example.com",
|
|
2386
|
-
duration: 500,
|
|
2387
|
-
samplecount: 3,
|
|
2388
|
-
hotfunctions: ["render"],
|
|
2389
|
-
at: stale,
|
|
2390
|
-
});
|
|
2391
|
-
expect((await store.getcpuprofiles())[0]?.samplesexpired).toBe(true);
|
|
2392
|
-
await store.settracerecord({
|
|
2393
|
-
id: "t1",
|
|
2394
|
-
runid: "plan",
|
|
2395
|
-
stepid: "tl",
|
|
2396
|
-
origin: "https://example.com",
|
|
2397
|
-
categories: ["scripting"],
|
|
2398
|
-
bytesize: 400,
|
|
2399
|
-
events: 2,
|
|
2400
|
-
annotations: [{ stepid: "s1", label: "open", offset: 100 }],
|
|
2401
|
-
startedat: stale,
|
|
2402
|
-
endedat: stale + 500,
|
|
2403
|
-
});
|
|
2404
|
-
const trace = (await store.gettracerecords())[0];
|
|
2405
|
-
expect(trace?.bytesexpired).toBe(true);
|
|
2406
|
-
expect(trace?.annotations).toHaveLength(1);
|
|
2407
|
-
await store.settracefile("t1", '{"devthinktrace":"1.1.47"}');
|
|
2408
|
-
expect(await store.gettracefile("t1")).toBeUndefined();
|
|
2409
|
-
});
|
|
2410
|
-
});
|
|
2411
|
-
|
|
2412
|
-
describe("emulation memory", () => {
|
|
2413
|
-
it("stores the emulation state per run with the layer history and expires reverted prior states after the retention window", async () => {
|
|
2414
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2415
|
-
const layer = {
|
|
2416
|
-
id: "l1",
|
|
2417
|
-
runid: "run",
|
|
2418
|
-
stepid: "s1",
|
|
2419
|
-
family: "device" as const,
|
|
2420
|
-
name: "phone",
|
|
2421
|
-
originscope: "https://example.com",
|
|
2422
|
-
appliedat: 1,
|
|
2423
|
-
prior: { pixelratio: 2 },
|
|
2424
|
-
revertplan: ["restore"],
|
|
2425
|
-
};
|
|
2426
|
-
await store.setemulationstate({
|
|
2427
|
-
runid: "run",
|
|
2428
|
-
tabid: 4,
|
|
2429
|
-
origin: "https://example.com",
|
|
2430
|
-
layers: [layer],
|
|
2431
|
-
updatedat: 2,
|
|
2432
|
-
});
|
|
2433
|
-
expect((await store.getemulationstate("run"))?.layers[0]?.name).toBe("phone");
|
|
2434
|
-
expect((await store.listlayers("run"))[0]?.revertplan).toEqual(["restore"]);
|
|
2435
|
-
expect(await store.listlayers("other")).toEqual([]);
|
|
2436
|
-
await store.setsettings({ emulationretention: 1 });
|
|
2437
|
-
await store.setemulationstate({
|
|
2438
|
-
runid: "run",
|
|
2439
|
-
tabid: 4,
|
|
2440
|
-
origin: "https://example.com",
|
|
2441
|
-
layers: [{ ...layer, revertedat: Date.now() - 5 }],
|
|
2442
|
-
updatedat: 3,
|
|
2443
|
-
});
|
|
2444
|
-
const expired = await store.listlayers("run");
|
|
2445
|
-
expect(expired[0]?.prior).toBeUndefined();
|
|
2446
|
-
expect(expired[0]?.priorexpired).toBe(true);
|
|
2447
|
-
expect(expired[0]?.name).toBe("phone");
|
|
2448
|
-
});
|
|
2449
|
-
|
|
2450
|
-
it("stores the user curated device, network, location and agent preset libraries", async () => {
|
|
2451
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2452
|
-
await store.setdevicepreset({ name: "phone", width: 390, height: 844, pixelratio: 3, mobile: true });
|
|
2453
|
-
await store.setdevicepreset({ name: "tablet", width: 820, height: 1180, pixelratio: 2, mobile: true });
|
|
2454
|
-
await store.setdevicepreset({ name: "phone", width: 391, height: 845, pixelratio: 3, mobile: true });
|
|
2455
|
-
expect((await store.getdevicepresets()).map((preset) => preset.name).sort()).toEqual(["phone", "tablet"]);
|
|
2456
|
-
expect((await store.getdevicepresets()).find((preset) => preset.name === "phone")?.width).toBe(391);
|
|
2457
|
-
await store.setnetworkpreset({ name: "slow3g", latency: 400, download: 400, upload: 400, offline: true });
|
|
2458
|
-
await store.setlocationpreset({ name: "lisbon", latitude: 38.7223, longitude: -9.1393, accuracy: 100 });
|
|
2459
|
-
await store.setagentpreset({
|
|
2460
|
-
name: "desktopmask",
|
|
2461
|
-
useragent: "Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0.0.0 Safari/537.36",
|
|
2462
|
-
platform: "Linux x86_64",
|
|
2463
|
-
brands: ["Chromium"],
|
|
2464
|
-
});
|
|
2465
|
-
expect((await store.getnetworkpresets())[0]?.offline).toBe(true);
|
|
2466
|
-
expect((await store.getlocationpresets())[0]?.latitude).toBe(38.7223);
|
|
2467
|
-
expect((await store.getagentpresets())[0]?.brands).toEqual(["Chromium"]);
|
|
2468
|
-
});
|
|
2469
|
-
|
|
2470
|
-
it("stores blackbox rule sets per origin, permission override history and location consents", async () => {
|
|
2471
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2472
|
-
await store.setblackboxrules("https://example.com", [
|
|
2473
|
-
{ urlpatterns: ["https://cdn.example/**"], tracescope: "both" },
|
|
2474
|
-
]);
|
|
2475
|
-
await store.setblackboxrules("https://example.com", [
|
|
2476
|
-
{ urlpatterns: ["https://vendor.example/*"], tracescope: "traces" },
|
|
2477
|
-
]);
|
|
2478
|
-
const rules = await store.getblackboxrules();
|
|
2479
|
-
expect(rules).toHaveLength(1);
|
|
2480
|
-
expect(rules[0]?.rules[0]?.urlpatterns).toEqual(["https://vendor.example/*"]);
|
|
2481
|
-
await store.addpermissionoverride({
|
|
2482
|
-
id: "p1",
|
|
2483
|
-
runid: "run",
|
|
2484
|
-
stepid: "s1",
|
|
2485
|
-
origin: "https://example.com",
|
|
2486
|
-
name: "geolocation",
|
|
2487
|
-
state: "granted",
|
|
2488
|
-
priorstate: "prompt",
|
|
2489
|
-
appliedat: 1,
|
|
2490
|
-
});
|
|
2491
|
-
await store.addpermissionoverride({
|
|
2492
|
-
id: "p1",
|
|
2493
|
-
runid: "run",
|
|
2494
|
-
stepid: "s1",
|
|
2495
|
-
origin: "https://example.com",
|
|
2496
|
-
name: "geolocation",
|
|
2497
|
-
state: "granted",
|
|
2498
|
-
priorstate: "prompt",
|
|
2499
|
-
appliedat: 1,
|
|
2500
|
-
restoredat: 2,
|
|
2501
|
-
});
|
|
2502
|
-
const overrides = await store.getpermissionoverrides();
|
|
2503
|
-
expect(overrides).toHaveLength(1);
|
|
2504
|
-
expect(overrides[0]?.restoredat).toBe(2);
|
|
2505
|
-
await store.setlocationconsent({
|
|
2506
|
-
id: "c1",
|
|
2507
|
-
prompt: "Where?",
|
|
2508
|
-
origin: "https://example.com",
|
|
2509
|
-
latitude: 38.7223,
|
|
2510
|
-
longitude: -9.1393,
|
|
2511
|
-
consentedat: 1,
|
|
2512
|
-
});
|
|
2513
|
-
await store.setlocationconsent({
|
|
2514
|
-
id: "c1",
|
|
2515
|
-
prompt: "Where?",
|
|
2516
|
-
origin: "https://example.com",
|
|
2517
|
-
latitude: 38.7223,
|
|
2518
|
-
longitude: -9.1393,
|
|
2519
|
-
approved: true,
|
|
2520
|
-
consentedat: 1,
|
|
2521
|
-
});
|
|
2522
|
-
const consents = await store.getlocationconsents();
|
|
2523
|
-
expect(consents).toHaveLength(1);
|
|
2524
|
-
expect(consents[0]?.approved).toBe(true);
|
|
2525
|
-
});
|
|
2526
|
-
});
|
|
2527
|
-
|
|
2528
|
-
describe("workflow editor memory", () => {
|
|
2529
|
-
it("stores workflow versions and version diffs for the timeline", async () => {
|
|
2530
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2531
|
-
await store.addworkflowversion({
|
|
2532
|
-
workflowid: "wf1",
|
|
2533
|
-
version: 1,
|
|
2534
|
-
createdat: 1,
|
|
2535
|
-
note: "first",
|
|
2536
|
-
steps: 2,
|
|
2537
|
-
risk: "read",
|
|
2538
|
-
});
|
|
2539
|
-
await store.addworkflowversion({
|
|
2540
|
-
workflowid: "wf1",
|
|
2541
|
-
version: 2,
|
|
2542
|
-
createdat: 2,
|
|
2543
|
-
note: "second",
|
|
2544
|
-
steps: 3,
|
|
2545
|
-
risk: "read",
|
|
2546
|
-
});
|
|
2547
|
-
await store.addworkflowversion({
|
|
2548
|
-
workflowid: "wf1",
|
|
2549
|
-
version: 2,
|
|
2550
|
-
createdat: 3,
|
|
2551
|
-
note: "second again",
|
|
2552
|
-
steps: 3,
|
|
2553
|
-
risk: "read",
|
|
2554
|
-
});
|
|
2555
|
-
const versions = await store.listworkflowversions("wf1");
|
|
2556
|
-
expect(versions.map((version) => version.note)).toEqual(["second again", "first"]);
|
|
2557
|
-
expect(await store.listworkflowversions("wf2")).toEqual([]);
|
|
2558
|
-
await store.addversiondiff({
|
|
2559
|
-
workflowid: "wf1",
|
|
2560
|
-
from: 1,
|
|
2561
|
-
to: 2,
|
|
2562
|
-
added: [{ stepid: "s3", kind: "wait", label: "Fresh" }],
|
|
2563
|
-
removed: [],
|
|
2564
|
-
changed: [],
|
|
2565
|
-
at: 4,
|
|
2566
|
-
});
|
|
2567
|
-
await store.addversiondiff({
|
|
2568
|
-
workflowid: "wf1",
|
|
2569
|
-
from: 1,
|
|
2570
|
-
to: 2,
|
|
2571
|
-
added: [],
|
|
2572
|
-
removed: [],
|
|
2573
|
-
changed: [{ stepid: "s1", kind: "readtext", label: "Open", changes: ["label"] }],
|
|
2574
|
-
at: 5,
|
|
2575
|
-
});
|
|
2576
|
-
const diffs = await store.listversiondiffs("wf1");
|
|
2577
|
-
expect(diffs).toHaveLength(1);
|
|
2578
|
-
expect(diffs[0]?.changed[0]?.changes).toEqual(["label"]);
|
|
2579
|
-
});
|
|
2580
|
-
|
|
2581
|
-
it("stores run history with retention and answers the filters", async () => {
|
|
2582
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2583
|
-
for (const [index, outcome] of ["done", "failed", "done", "paused"].entries()) {
|
|
2584
|
-
await store.addrunhistory({
|
|
2585
|
-
runid: `r${index}`,
|
|
2586
|
-
workflowid: index % 2 === 0 ? "wf1" : "wf2",
|
|
2587
|
-
outcome,
|
|
2588
|
-
steps: index,
|
|
2589
|
-
total: 4,
|
|
2590
|
-
duration: index,
|
|
2591
|
-
cause: "manual",
|
|
2592
|
-
startedat: 10 + index,
|
|
2593
|
-
endedat: 20 + index,
|
|
2594
|
-
});
|
|
2595
|
-
}
|
|
2596
|
-
expect((await store.gethistory()).map((entry) => entry.runid)).toEqual(["r3", "r2", "r1", "r0"]);
|
|
2597
|
-
expect((await store.gethistory({ workflowid: "wf1" })).map((entry) => entry.runid)).toEqual(["r2", "r0"]);
|
|
2598
|
-
expect((await store.gethistory({ outcome: "done", limit: 1 })).map((entry) => entry.runid)).toEqual(["r2"]);
|
|
2599
|
-
expect((await store.gethistory({ since: 23 })).map((entry) => entry.runid)).toEqual(["r3"]);
|
|
2600
|
-
await store.setsettings({ runhistoryretention: 2 });
|
|
2601
|
-
await store.addrunhistory({
|
|
2602
|
-
runid: "r4",
|
|
2603
|
-
workflowid: "wf1",
|
|
2604
|
-
outcome: "done",
|
|
2605
|
-
steps: 4,
|
|
2606
|
-
total: 4,
|
|
2607
|
-
duration: 1,
|
|
2608
|
-
cause: "cron",
|
|
2609
|
-
startedat: 30,
|
|
2610
|
-
endedat: 31,
|
|
2611
|
-
dryrun: true,
|
|
2612
|
-
});
|
|
2613
|
-
const kept = await store.gethistory();
|
|
2614
|
-
expect(kept.map((entry) => entry.runid)).toEqual(["r4", "r3"]);
|
|
2615
|
-
expect(kept[0]?.dryrun).toBe(true);
|
|
2616
|
-
expect(kept[0]?.cause).toBe("cron");
|
|
2617
|
-
});
|
|
2618
|
-
|
|
2619
|
-
it("stores editor layouts, breakpoints, overrides, watchdog events, imports and background flags", async () => {
|
|
2620
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2621
|
-
await store.seteditorlayout("wf1", { width: 900, height: 700, viewportx: 12, viewporty: 34, zoom: 1.5 });
|
|
2622
|
-
expect(await store.geteditorlayout("wf1")).toEqual({
|
|
2623
|
-
width: 900,
|
|
2624
|
-
height: 700,
|
|
2625
|
-
viewportx: 12,
|
|
2626
|
-
viewporty: 34,
|
|
2627
|
-
zoom: 1.5,
|
|
2628
|
-
});
|
|
2629
|
-
expect(await store.geteditorlayout("wf2")).toBeUndefined();
|
|
2630
|
-
await store.setworkflowbreakpoints("wf1", ["s1", "s3"]);
|
|
2631
|
-
await store.setworkflowbreakpoints("wf1", ["s2"]);
|
|
2632
|
-
expect(await store.getworkflowbreakpoints("wf1")).toEqual(["s2"]);
|
|
2633
|
-
await store.addsiteoverride({
|
|
2634
|
-
id: "o1",
|
|
2635
|
-
workflowid: "wf1",
|
|
2636
|
-
pattern: "https://example.com",
|
|
2637
|
-
deltas: { waitms: 500 },
|
|
2638
|
-
createdat: 1,
|
|
2639
|
-
});
|
|
2640
|
-
await store.addsiteoverride({
|
|
2641
|
-
id: "o1",
|
|
2642
|
-
workflowid: "wf1",
|
|
2643
|
-
pattern: "https://example.com",
|
|
2644
|
-
deltas: { delaybase: 250 },
|
|
2645
|
-
createdat: 2,
|
|
2646
|
-
});
|
|
2647
|
-
await store.addsiteoverride({
|
|
2648
|
-
id: "o2",
|
|
2649
|
-
workflowid: "wf2",
|
|
2650
|
-
pattern: "https://other.example",
|
|
2651
|
-
deltas: {},
|
|
2652
|
-
createdat: 3,
|
|
2653
|
-
});
|
|
2654
|
-
expect((await store.listsiteoverrides("wf1")).map((override) => override.deltas)).toEqual([{ delaybase: 250 }]);
|
|
2655
|
-
await store.removesiteoverride("o2");
|
|
2656
|
-
expect(await store.listsiteoverrides()).toHaveLength(1);
|
|
2657
|
-
await store.addwatchdogevent({
|
|
2658
|
-
id: "w1",
|
|
2659
|
-
runid: "r1",
|
|
2660
|
-
verdict: "stalled",
|
|
2661
|
-
action: "pause",
|
|
2662
|
-
outcome: "Paused.",
|
|
2663
|
-
at: 1,
|
|
2664
|
-
});
|
|
2665
|
-
await store.addwatchdogevent({
|
|
2666
|
-
id: "w2",
|
|
2667
|
-
runid: "r2",
|
|
2668
|
-
verdict: "zombie",
|
|
2669
|
-
action: "reap",
|
|
2670
|
-
outcome: "Reaped.",
|
|
2671
|
-
at: 2,
|
|
2672
|
-
});
|
|
2673
|
-
expect((await store.listwatchdogevents()).map((event) => event.verdict)).toEqual(["zombie", "stalled"]);
|
|
2674
|
-
const record = {
|
|
2675
|
-
id: "wf9",
|
|
2676
|
-
name: "imported",
|
|
2677
|
-
version: 1,
|
|
2678
|
-
origins: ["https://example.com"],
|
|
2679
|
-
steps: [],
|
|
2680
|
-
blocks: [],
|
|
2681
|
-
risk: "read" as const,
|
|
2682
|
-
createdat: 1,
|
|
2683
|
-
};
|
|
2684
|
-
await store.addworkflowimport({ id: "imp1", record, importedat: 5, filename: "flow.json" });
|
|
2685
|
-
expect((await store.listworkflowimports()).map((entry) => entry.filename)).toEqual(["flow.json"]);
|
|
2686
|
-
await store.removeworkflowimport("imp1");
|
|
2687
|
-
expect(await store.listworkflowimports()).toEqual([]);
|
|
2688
|
-
await store.setbackgroundruns({ wf1: true });
|
|
2689
|
-
expect(await store.getbackgroundruns()).toEqual({ wf1: true });
|
|
2690
|
-
await store.addworkflowrecord(record);
|
|
2691
|
-
await store.removeworkflowversion("wf9", 1);
|
|
2692
|
-
expect(await store.listworkflows()).toEqual([]);
|
|
2693
|
-
});
|
|
2694
|
-
});
|
|
2695
|
-
|
|
2696
|
-
describe("agent protocol memory", () => {
|
|
2697
|
-
it("stores client records with pairing state, negotiated capability sets and the server config and state", async () => {
|
|
2698
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2699
|
-
const client = { id: "client1", transport: "stdio" as const, paired: false, connectedat: 10 };
|
|
2700
|
-
await store.setclient(client);
|
|
2701
|
-
await store.setclient({ ...client, id: "client2", transport: "http" as const, connectedat: 11 });
|
|
2702
|
-
expect((await store.listclients()).map((entry) => entry.id)).toEqual(["client2", "client1"]);
|
|
2703
|
-
await store.setclient({ ...client, paired: true, pairedat: 12 });
|
|
2704
|
-
expect((await store.listclients()).find((entry) => entry.id === "client1")?.paired).toBe(true);
|
|
2705
|
-
const capabilities = {
|
|
2706
|
-
protocolversion: "1.1.54",
|
|
2707
|
-
name: "devthink",
|
|
2708
|
-
version: "1.1.54",
|
|
2709
|
-
toolversion: 1,
|
|
2710
|
-
tools: 30,
|
|
2711
|
-
namespaces: ["browser" as const],
|
|
2712
|
-
transports: ["stdio" as const],
|
|
2713
|
-
};
|
|
2714
|
-
await store.setclientcapabilities("client1", capabilities);
|
|
2715
|
-
expect((await store.getclients()).find((entry) => entry.id === "client1")?.capabilities?.toolversion).toBe(1);
|
|
2716
|
-
await store.setclient({
|
|
2717
|
-
...client,
|
|
2718
|
-
id: "client2",
|
|
2719
|
-
transport: "http" as const,
|
|
2720
|
-
connectedat: 11,
|
|
2721
|
-
disconnectedat: 13,
|
|
2722
|
-
});
|
|
2723
|
-
expect((await store.listclients()).map((entry) => entry.id)).toEqual(["client1"]);
|
|
2724
|
-
await store.clearclients();
|
|
2725
|
-
expect(await store.listclients()).toEqual([]);
|
|
2726
|
-
await store.setmcpconfig({
|
|
2727
|
-
port: 7436,
|
|
2728
|
-
transports: ["stdio"],
|
|
2729
|
-
enabled: true,
|
|
2730
|
-
framesize: 50_000,
|
|
2731
|
-
queuedepth: 20,
|
|
2732
|
-
callretention: 10,
|
|
2733
|
-
});
|
|
2734
|
-
expect((await store.getmcpconfig())?.framesize).toBe(50_000);
|
|
2735
|
-
await store.setmcpstate({
|
|
2736
|
-
state: "running",
|
|
2737
|
-
startedat: 20,
|
|
2738
|
-
bridge: {
|
|
2739
|
-
id: "bridge1",
|
|
2740
|
-
host: "com.wenathlan.devthink",
|
|
2741
|
-
connected: true,
|
|
2742
|
-
pid: 4242,
|
|
2743
|
-
startedat: 20,
|
|
2744
|
-
restarts: 0,
|
|
2745
|
-
received: 0,
|
|
2746
|
-
sent: 0,
|
|
2747
|
-
},
|
|
2748
|
-
});
|
|
2749
|
-
expect((await store.getmcpstate())?.bridge?.pid).toBe(4242);
|
|
2750
|
-
expect(await store.getmcpstate()).toMatchObject({ state: "running" });
|
|
2751
|
-
});
|
|
2752
|
-
|
|
2753
|
-
it("stores bridge launch events with process ids and tool call records under the user configured retention", async () => {
|
|
2754
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2755
|
-
await store.addbridgelaunch({ id: "l1", host: "com.wenathlan.devthink", pid: 4242, restart: false, at: 30 });
|
|
2756
|
-
await store.addbridgelaunch({ id: "l2", host: "com.wenathlan.devthink", pid: 5252, restart: true, at: 31 });
|
|
2757
|
-
expect((await store.listbridgelaunches()).map((launch) => launch.pid)).toEqual([5252, 4242]);
|
|
2758
|
-
await store.addtoolcall({
|
|
2759
|
-
id: "t1",
|
|
2760
|
-
clientid: "client1",
|
|
2761
|
-
tool: "browser.click",
|
|
2762
|
-
origin: "https://example.com",
|
|
2763
|
-
ok: true,
|
|
2764
|
-
at: 40,
|
|
2765
|
-
});
|
|
2766
|
-
await store.addtoolcall({
|
|
2767
|
-
id: "t2",
|
|
2768
|
-
clientid: "client1",
|
|
2769
|
-
tool: "browser.readtext",
|
|
2770
|
-
origin: "https://example.com",
|
|
2771
|
-
ok: false,
|
|
2772
|
-
code: "consentrefused",
|
|
2773
|
-
at: 41,
|
|
2774
|
-
});
|
|
2775
|
-
expect((await store.listtoolcalls()).map((call) => call.id)).toEqual(["t2", "t1"]);
|
|
2776
|
-
await store.setmcpconfig({ port: 7436, transports: ["stdio"], enabled: true, callretention: 1 });
|
|
2777
|
-
await store.addtoolcall({
|
|
2778
|
-
id: "t3",
|
|
2779
|
-
clientid: "client2",
|
|
2780
|
-
tool: "browser.snapshot",
|
|
2781
|
-
origin: "https://example.com",
|
|
2782
|
-
ok: true,
|
|
2783
|
-
at: 42,
|
|
2784
|
-
});
|
|
2785
|
-
expect((await store.listtoolcalls()).map((call) => call.id)).toEqual(["t3"]);
|
|
2786
|
-
});
|
|
2787
|
-
|
|
2788
|
-
it("records tool list, negotiate, connect and tool call audit events", async () => {
|
|
2789
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2790
|
-
await store.addaudi({
|
|
2791
|
-
id: "a1",
|
|
2792
|
-
kind: "protocol",
|
|
2793
|
-
at: 50,
|
|
2794
|
-
summary: "A new mcp client client1 connected on the stdio transport and waits for the pairing approval.",
|
|
2795
|
-
});
|
|
2796
|
-
await store.addaudi({
|
|
2797
|
-
id: "a2",
|
|
2798
|
-
kind: "protocol",
|
|
2799
|
-
at: 51,
|
|
2800
|
-
summary: "The mcp client client1 listed the tool catalog of 30 tools.",
|
|
2801
|
-
});
|
|
2802
|
-
await store.addaudi({
|
|
2803
|
-
id: "a3",
|
|
2804
|
-
kind: "protocol",
|
|
2805
|
-
at: 52,
|
|
2806
|
-
summary: "The mcp client client1 negotiated its capability set with the server.",
|
|
2807
|
-
});
|
|
2808
|
-
await store.addaudi({
|
|
2809
|
-
id: "a4",
|
|
2810
|
-
kind: "tool",
|
|
2811
|
-
at: 53,
|
|
2812
|
-
summary:
|
|
2813
|
-
"The mcp client client1 called the browser.click tool on https://example.com and it ran behind the consent gates.",
|
|
2814
|
-
sessionid: "sess",
|
|
2815
|
-
});
|
|
2816
|
-
const audit = await store.getaudit();
|
|
2817
|
-
expect(audit.map((event) => event.kind)).toEqual(["tool", "protocol", "protocol", "protocol"]);
|
|
2818
|
-
expect(audit[0]?.summary).toContain("browser.click");
|
|
2819
|
-
expect(audit[3]?.summary).toContain("pairing approval");
|
|
2820
|
-
});
|
|
2821
|
-
});
|
|
2822
|
-
|
|
2823
|
-
describe("sessionmemory agent protocol part two", () => {
|
|
2824
|
-
const now = 1_800_000_000_000;
|
|
2825
|
-
|
|
2826
|
-
it("stores session tokens only in their tokenhash form", async () => {
|
|
2827
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2828
|
-
await store.setsessiontokens([
|
|
2829
|
-
{ id: "t1", clientid: "client1", hash: "sha256:abc", scopes: ["browser"], issuedat: now, expiresat: now + 1000 },
|
|
2830
|
-
]);
|
|
2831
|
-
const tokens = await store.getsessiontokens();
|
|
2832
|
-
expect(tokens[0]?.hash).toBe("sha256:abc");
|
|
2833
|
-
expect(JSON.stringify(tokens)).not.toContain("rawtoken");
|
|
2834
|
-
await store.setsessiontokens([
|
|
2835
|
-
...tokens,
|
|
2836
|
-
{ id: "t2", clientid: "client2", hash: "sha256:def", scopes: ["memory"], issuedat: now, expiresat: now + 1000 },
|
|
2837
|
-
]);
|
|
2838
|
-
expect((await store.getsessiontokens()).map((token) => token.id)).toEqual(["t1", "t2"]);
|
|
2839
|
-
});
|
|
2840
|
-
|
|
2841
|
-
it("stores pairing codes with their single use state", async () => {
|
|
2842
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2843
|
-
await store.addpairingcode({ code: "DT-ONE", scopes: ["browser"], issuedat: now, expiresat: now + 1000 });
|
|
2844
|
-
await store.addpairingcode({ code: "DT-TWO", scopes: ["memory"], issuedat: now, expiresat: now + 1000 });
|
|
2845
|
-
expect((await store.getpairingcodes()).map((code) => code.code)).toEqual(["DT-TWO", "DT-ONE"]);
|
|
2846
|
-
await store.addpairingcode({ code: "DT-TWO", scopes: ["system"], issuedat: now, expiresat: now + 1000 });
|
|
2847
|
-
expect((await store.getpairingcodes()).filter((code) => code.code === "DT-TWO")).toHaveLength(1);
|
|
2848
|
-
await store.usepairingcode("DT-ONE", now + 500);
|
|
2849
|
-
expect((await store.getpairingcodes()).find((code) => code.code === "DT-ONE")?.usedat).toBe(now + 500);
|
|
2850
|
-
await store.usepairingcode("DT-MISSING", now);
|
|
2851
|
-
expect((await store.getpairingcodes()).find((code) => code.code === "DT-ONE")?.usedat).toBe(now + 500);
|
|
2852
|
-
});
|
|
2853
|
-
|
|
2854
|
-
it("stores allowlist entries, client identities and stream channels", async () => {
|
|
2855
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2856
|
-
await store.setclientidentity({ fingerprint: "aa11", displayname: "Laptop agent" });
|
|
2857
|
-
await store.setallowlistentry({
|
|
2858
|
-
fingerprint: "aa11",
|
|
2859
|
-
displayname: "Laptop agent",
|
|
2860
|
-
namespaces: ["browser"],
|
|
2861
|
-
grantedat: now,
|
|
2862
|
-
history: [{ at: now, actor: "user", change: "Granted." }],
|
|
2863
|
-
});
|
|
2864
|
-
await store.setallowlistentry({
|
|
2865
|
-
fingerprint: "bb22",
|
|
2866
|
-
displayname: "Desk agent",
|
|
2867
|
-
namespaces: ["memory"],
|
|
2868
|
-
grantedat: now,
|
|
2869
|
-
history: [],
|
|
2870
|
-
});
|
|
2871
|
-
expect((await store.getallowlist()).map((entry) => entry.fingerprint)).toEqual(["bb22", "aa11"]);
|
|
2872
|
-
await store.setallowlistentry({
|
|
2873
|
-
fingerprint: "aa11",
|
|
2874
|
-
displayname: "Laptop agent",
|
|
2875
|
-
namespaces: ["system"],
|
|
2876
|
-
grantedat: now,
|
|
2877
|
-
history: [{ at: now + 1, actor: "user", change: "Rescoped." }],
|
|
2878
|
-
});
|
|
2879
|
-
expect((await store.getallowlist()).find((entry) => entry.fingerprint === "aa11")?.namespaces).toEqual(["system"]);
|
|
2880
|
-
await store.removeallowlistentry("bb22");
|
|
2881
|
-
expect((await store.getallowlist()).map((entry) => entry.fingerprint)).toEqual(["aa11"]);
|
|
2882
|
-
expect((await store.getclientidentities())[0]?.displayname).toBe("Laptop agent");
|
|
2883
|
-
await store.setstreamchannels([{ id: "c1", clientid: "client1", openedat: now, lastbeatat: now }]);
|
|
2884
|
-
await store.setstreamchannels([
|
|
2885
|
-
{ id: "c1", clientid: "client1", openedat: now, lastbeatat: now, closedat: now + 5 },
|
|
2886
|
-
]);
|
|
2887
|
-
expect((await store.getstreamchannels())[0]?.closedat).toBe(now + 5);
|
|
2888
|
-
});
|
|
2889
|
-
|
|
2890
|
-
it("stores approval gates with their decisions and auth handshake events", async () => {
|
|
2891
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2892
|
-
const gate = {
|
|
2893
|
-
id: "gate-1",
|
|
2894
|
-
clientid: "client1",
|
|
2895
|
-
tool: "browser.click",
|
|
2896
|
-
reason: "Click.",
|
|
2897
|
-
params: { stepid: "s1" },
|
|
2898
|
-
state: "pending" as const,
|
|
2899
|
-
raisedat: now,
|
|
2900
|
-
};
|
|
2901
|
-
await store.setapproval(gate);
|
|
2902
|
-
await store.setapproval({ ...gate, state: "approved", decidedat: now + 1000, actor: "user" });
|
|
2903
|
-
const approvals = await store.listapprovals();
|
|
2904
|
-
expect(approvals).toHaveLength(1);
|
|
2905
|
-
expect(approvals[0]?.state).toBe("approved");
|
|
2906
|
-
await store.addapprovalexec({
|
|
2907
|
-
requestid: "gate-1",
|
|
2908
|
-
decision: "approved",
|
|
2909
|
-
actor: "user",
|
|
2910
|
-
at: now + 1000,
|
|
2911
|
-
latencyms: 1000,
|
|
2912
|
-
});
|
|
2913
|
-
expect((await store.listapprovalexecs())[0]?.latencyms).toBe(1000);
|
|
2914
|
-
await store.addauthhandshake({
|
|
2915
|
-
id: "h1",
|
|
2916
|
-
clientid: "client1",
|
|
2917
|
-
method: "pairingcode",
|
|
2918
|
-
outcome: "verified",
|
|
2919
|
-
at: now,
|
|
2920
|
-
});
|
|
2921
|
-
await store.addauthhandshake({ id: "h2", clientid: "new", method: "token", outcome: "refused", at: now + 1 });
|
|
2922
|
-
expect((await store.listauthhandshakes()).map((event) => event.outcome)).toEqual(["refused", "verified"]);
|
|
2923
|
-
});
|
|
2924
|
-
});
|
|
2925
|
-
|
|
2926
|
-
describe("sessionmemory agent protocol part three", () => {
|
|
2927
|
-
const now = 1_800_000_000_000;
|
|
2928
|
-
|
|
2929
|
-
it("stores subscriptions, watches and sampling requests with their provenance", async () => {
|
|
2930
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2931
|
-
await store.seteventsubscriptions([{ id: "sub1", clientid: "client1", kinds: ["callresult"], createdat: now }]);
|
|
2932
|
-
expect((await store.geteventsubscriptions()).map((subscription) => subscription.id)).toEqual(["sub1"]);
|
|
2933
|
-
await store.seteventsubscriptions([{ id: "sub2", clientid: "client2", kinds: ["progress"], createdat: now + 1 }]);
|
|
2934
|
-
expect((await store.geteventsubscriptions()).map((subscription) => subscription.id)).toEqual(["sub2"]);
|
|
2935
|
-
await store.setresourcewatches([
|
|
2936
|
-
{ id: "watch1", clientid: "client1", resource: "page", baseline: { url: "https://example.com" }, createdat: now },
|
|
2937
|
-
]);
|
|
2938
|
-
expect((await store.getresourcewatches())[0]?.baseline).toEqual({ url: "https://example.com" });
|
|
2939
|
-
const sampling = {
|
|
2940
|
-
id: "sample1",
|
|
2941
|
-
clientid: "client1",
|
|
2942
|
-
prompt: "Summarize.",
|
|
2943
|
-
state: "pending" as const,
|
|
2944
|
-
requestedat: now,
|
|
2945
|
-
};
|
|
2946
|
-
await store.setsamplingrequests([sampling]);
|
|
2947
|
-
await store.setsamplingrequests([
|
|
2948
|
-
{ ...sampling, state: "answered" as const, answeredat: now + 1, answer: "the model answer" },
|
|
2949
|
-
]);
|
|
2950
|
-
expect((await store.getsamplingrequests())[0]?.answer).toBe("the model answer");
|
|
2951
|
-
});
|
|
2952
|
-
|
|
2953
|
-
it("stores idempotency records, rate limit counters and batch outcomes", async () => {
|
|
2954
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2955
|
-
await store.setidempotencyrecords([
|
|
2956
|
-
{
|
|
2957
|
-
key: "key-1",
|
|
2958
|
-
clientid: "client1",
|
|
2959
|
-
tool: "browser.readtext",
|
|
2960
|
-
result: { content: "stored", iserror: false },
|
|
2961
|
-
createdat: now,
|
|
2962
|
-
expiresat: now + 5000,
|
|
2963
|
-
},
|
|
2964
|
-
]);
|
|
2965
|
-
expect((await store.getidempotencyrecords())[0]?.key).toBe("key-1");
|
|
2966
|
-
await store.setcallratelimits([
|
|
2967
|
-
{ clientid: "client1", windowms: 60_000, budget: 5, windowstartedat: now, used: 3 },
|
|
2968
|
-
]);
|
|
2969
|
-
expect((await store.getcallratelimits())[0]?.used).toBe(3);
|
|
2970
|
-
const batch = {
|
|
2971
|
-
id: "batch1",
|
|
2972
|
-
clientid: "client1",
|
|
2973
|
-
calls: [{ id: "m1", name: "browser.readtext", params: {} }],
|
|
2974
|
-
stoponerror: true,
|
|
2975
|
-
state: "running" as const,
|
|
2976
|
-
createdat: now,
|
|
2977
|
-
outcomes: [],
|
|
2978
|
-
};
|
|
2979
|
-
await store.setbatchcall(batch);
|
|
2980
|
-
await store.setbatchcall({
|
|
2981
|
-
...batch,
|
|
2982
|
-
state: "done" as const,
|
|
2983
|
-
finishedat: now + 1,
|
|
2984
|
-
outcomes: [{ callid: "m1", tool: "browser.readtext", ok: true, at: now + 1 }],
|
|
2985
|
-
});
|
|
2986
|
-
const stored = await store.getbatchcalls();
|
|
2987
|
-
expect(stored).toHaveLength(1);
|
|
2988
|
-
expect(stored[0]?.outcomes[0]?.ok).toBe(true);
|
|
2989
|
-
});
|
|
2990
|
-
|
|
2991
|
-
it("stores call contexts, chunks, progress notices, mocks and the dry run toggle", async () => {
|
|
2992
|
-
const store = new sessionmemory(new fakeadapter());
|
|
2993
|
-
await store.setcallcontexts([
|
|
2994
|
-
{ callid: "call1", clientid: "client1", tool: "browser.readtext", state: "inflight", startedat: now, chunks: 0 },
|
|
2995
|
-
]);
|
|
2996
|
-
expect((await store.getcallcontexts())[0]?.state).toBe("inflight");
|
|
2997
|
-
for (let index = 0; index < 30; index += 1)
|
|
2998
|
-
await store.addstreamchunk({
|
|
2999
|
-
callid: "call1",
|
|
3000
|
-
seq: index + 1,
|
|
3001
|
-
content: "x",
|
|
3002
|
-
done: index === 29,
|
|
3003
|
-
at: now + index,
|
|
3004
|
-
});
|
|
3005
|
-
expect(await store.getstreamchunks()).toHaveLength(25);
|
|
3006
|
-
for (let index = 0; index < 30; index += 1)
|
|
3007
|
-
await store.addprogressnotice({ callid: "call1", message: "working", cancellable: true, at: now + index });
|
|
3008
|
-
expect(await store.getprogressnotices()).toHaveLength(25);
|
|
3009
|
-
await store.settoolmock({
|
|
3010
|
-
tool: "browser.readtext",
|
|
3011
|
-
result: { content: "canned", iserror: false },
|
|
3012
|
-
testcontext: true,
|
|
3013
|
-
createdat: now,
|
|
3014
|
-
});
|
|
3015
|
-
expect((await store.gettoolmocks())[0]?.tool).toBe("browser.readtext");
|
|
3016
|
-
await store.removetoolmock("browser.readtext");
|
|
3017
|
-
expect(await store.gettoolmocks()).toHaveLength(0);
|
|
3018
|
-
await store.setdryruntoggle(true);
|
|
3019
|
-
expect(await store.getdryruntoggle()).toBe(true);
|
|
3020
|
-
await store.setdryruntoggle(false);
|
|
3021
|
-
expect(await store.getdryruntoggle()).toBe(false);
|
|
3022
|
-
});
|
|
3023
|
-
|
|
3024
|
-
it("returns the audited call log under the requested filters", async () => {
|
|
3025
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3026
|
-
const one = {
|
|
3027
|
-
id: "call1",
|
|
3028
|
-
clientid: "client1",
|
|
3029
|
-
tool: "browser.readtext",
|
|
3030
|
-
origin: "https://example.com",
|
|
3031
|
-
ok: true,
|
|
3032
|
-
at: now,
|
|
3033
|
-
idempotencykey: "key-1",
|
|
3034
|
-
};
|
|
3035
|
-
const two = {
|
|
3036
|
-
id: "call2",
|
|
3037
|
-
clientid: "client2",
|
|
3038
|
-
tool: "memory.list",
|
|
3039
|
-
origin: "https://example.com",
|
|
3040
|
-
ok: false,
|
|
3041
|
-
code: "consentrefused" as const,
|
|
3042
|
-
at: now + 1,
|
|
3043
|
-
};
|
|
3044
|
-
const three = {
|
|
3045
|
-
id: "call3",
|
|
3046
|
-
clientid: "client1",
|
|
3047
|
-
tool: "browser.click",
|
|
3048
|
-
origin: "https://example.com",
|
|
3049
|
-
ok: true,
|
|
3050
|
-
at: now + 2,
|
|
3051
|
-
dryrun: true,
|
|
3052
|
-
};
|
|
3053
|
-
await store.addtoolcall(one);
|
|
3054
|
-
await store.addtoolcall(two);
|
|
3055
|
-
await store.addtoolcall(three);
|
|
3056
|
-
expect((await store.getcalllog()).map((record) => record.id)).toEqual(["call3", "call2", "call1"]);
|
|
3057
|
-
expect((await store.getcalllog({ clientid: "client1" })).map((record) => record.id)).toEqual(["call3", "call1"]);
|
|
3058
|
-
expect((await store.getcalllog({ tool: "memory.list" })).map((record) => record.id)).toEqual(["call2"]);
|
|
3059
|
-
expect((await store.getcalllog({ ok: true })).map((record) => record.id)).toEqual(["call3", "call1"]);
|
|
3060
|
-
expect((await store.getcalllog({ since: now + 1 })).map((record) => record.id)).toEqual(["call3", "call2"]);
|
|
3061
|
-
expect((await store.getcalllog({ limit: 1 })).map((record) => record.id)).toEqual(["call3"]);
|
|
3062
|
-
expect((await store.getcalllog())[2]?.idempotencykey).toBe("key-1");
|
|
3063
|
-
expect((await store.getcalllog())[0]?.dryrun).toBe(true);
|
|
3064
|
-
});
|
|
3065
|
-
});
|
|
3066
|
-
|
|
3067
|
-
describe("sessionmemory llm integration", () => {
|
|
3068
|
-
const now = 1_800_000_000_000;
|
|
3069
|
-
|
|
3070
|
-
it("stores provider configs with references, never secrets, and the routing tables with revision history", async () => {
|
|
3071
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3072
|
-
const provider: providerconfig = {
|
|
3073
|
-
id: "prov1",
|
|
3074
|
-
name: "The gateway",
|
|
3075
|
-
endpoint: "https://gateway.example/v1",
|
|
3076
|
-
style: "chatcompletions",
|
|
3077
|
-
models: ["model-a"],
|
|
3078
|
-
status: "available",
|
|
3079
|
-
authref: {
|
|
3080
|
-
name: "gateway key",
|
|
3081
|
-
origins: ["https://gateway.example"],
|
|
3082
|
-
header: "authorization",
|
|
3083
|
-
storageid: "key-store-1",
|
|
3084
|
-
configuredat: now,
|
|
3085
|
-
},
|
|
3086
|
-
createdat: now,
|
|
3087
|
-
};
|
|
3088
|
-
await store.setproviders([provider]);
|
|
3089
|
-
const stored = (await store.getproviders())[0];
|
|
3090
|
-
expect(stored?.authref?.storageid).toBe("key-store-1");
|
|
3091
|
-
expect(JSON.stringify(stored)).not.toContain("the key material");
|
|
3092
|
-
const route = {
|
|
3093
|
-
id: "route1",
|
|
3094
|
-
kind: "draftplan",
|
|
3095
|
-
providerid: "prov1",
|
|
3096
|
-
model: "model-a",
|
|
3097
|
-
revision: 2,
|
|
3098
|
-
updatedat: now,
|
|
3099
|
-
};
|
|
3100
|
-
await store.setmodelroutes([route]);
|
|
3101
|
-
await store.addmodelrouterevision(route);
|
|
3102
|
-
expect((await store.getmodelroutes())[0]?.revision).toBe(2);
|
|
3103
|
-
expect((await store.getmodelroutehistory())[0]?.kind).toBe("draftplan");
|
|
3104
|
-
});
|
|
3105
|
-
|
|
3106
|
-
it("stores every usage record with run and step ids and answers the period totals", async () => {
|
|
3107
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3108
|
-
const record: usagerecord = {
|
|
3109
|
-
id: "u1",
|
|
3110
|
-
runid: "run1",
|
|
3111
|
-
stepid: "s1",
|
|
3112
|
-
providerid: "prov1",
|
|
3113
|
-
endpoint: "https://gateway.example/v1",
|
|
3114
|
-
model: "model-a",
|
|
3115
|
-
prompttokens: 10,
|
|
3116
|
-
completiontokens: 5,
|
|
3117
|
-
totaltokens: 15,
|
|
3118
|
-
cost: 0.01,
|
|
3119
|
-
at: now,
|
|
3120
|
-
};
|
|
3121
|
-
const other: usagerecord = {
|
|
3122
|
-
id: "u2",
|
|
3123
|
-
runid: "run2",
|
|
3124
|
-
providerid: "prov1",
|
|
3125
|
-
endpoint: "https://gateway.example/v1",
|
|
3126
|
-
model: "model-a",
|
|
3127
|
-
prompttokens: 100,
|
|
3128
|
-
completiontokens: 50,
|
|
3129
|
-
totaltokens: 150,
|
|
3130
|
-
cost: 0.1,
|
|
3131
|
-
at: now + 1000,
|
|
3132
|
-
};
|
|
3133
|
-
await store.addusagerecord(record);
|
|
3134
|
-
await store.addusagerecord(other);
|
|
3135
|
-
expect((await store.getusagerecords())[0]?.id).toBe("u2");
|
|
3136
|
-
expect(await store.getusage()).toEqual({
|
|
3137
|
-
prompttokens: 110,
|
|
3138
|
-
completiontokens: 55,
|
|
3139
|
-
totaltokens: 165,
|
|
3140
|
-
cost: 0.11,
|
|
3141
|
-
calls: 2,
|
|
3142
|
-
});
|
|
3143
|
-
expect((await store.getusage({ runid: "run1" })).calls).toBe(1);
|
|
3144
|
-
expect((await store.getusage({ since: now + 1 })).totaltokens).toBe(150);
|
|
3145
|
-
});
|
|
3146
|
-
|
|
3147
|
-
it("stores plandrafts, replans and reflectnotes for the audit history", async () => {
|
|
3148
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3149
|
-
const draft: plandraft = {
|
|
3150
|
-
id: "d1",
|
|
3151
|
-
goal: "Read the page",
|
|
3152
|
-
steps: [{ id: "s1", kind: "observe", summary: "Observe." }],
|
|
3153
|
-
openquestions: [],
|
|
3154
|
-
providerid: "prov1",
|
|
3155
|
-
model: "model-a",
|
|
3156
|
-
state: "approved",
|
|
3157
|
-
lintfindings: [],
|
|
3158
|
-
createdat: now,
|
|
3159
|
-
};
|
|
3160
|
-
await store.addplandraft(draft);
|
|
3161
|
-
expect((await store.getplandrafts())[0]?.goal).toBe("Read the page");
|
|
3162
|
-
await store.setplandrafts([{ ...draft, state: "rejected" }]);
|
|
3163
|
-
expect((await store.getplandrafts())[0]?.state).toBe("rejected");
|
|
3164
|
-
const replan: replanrecord = {
|
|
3165
|
-
id: "r1",
|
|
3166
|
-
draftid: "d1",
|
|
3167
|
-
completedstepids: ["s1"],
|
|
3168
|
-
failedstepids: [],
|
|
3169
|
-
tail: [{ id: "t1", kind: "reload", summary: "Reload.", freshreview: true }],
|
|
3170
|
-
reason: "The step timed out.",
|
|
3171
|
-
providerid: "prov1",
|
|
3172
|
-
model: "model-a",
|
|
3173
|
-
state: "pending",
|
|
3174
|
-
createdat: now,
|
|
3175
|
-
};
|
|
3176
|
-
await store.addreplan(replan);
|
|
3177
|
-
expect((await store.getreplans())[0]?.tail[0]?.freshreview).toBe(true);
|
|
3178
|
-
const note: reflectnote = {
|
|
3179
|
-
id: "n1",
|
|
3180
|
-
runid: "run1",
|
|
3181
|
-
stepid: "s1",
|
|
3182
|
-
outcome: "The step landed.",
|
|
3183
|
-
lesson: "Wait before the click.",
|
|
3184
|
-
advice: "Add a settle window.",
|
|
3185
|
-
providerid: "prov1",
|
|
3186
|
-
model: "model-a",
|
|
3187
|
-
createdat: now,
|
|
3188
|
-
};
|
|
3189
|
-
await store.addreflectnote(note);
|
|
3190
|
-
expect((await store.getreflectnotes())[0]?.lesson).toMatch(/Wait before the click/);
|
|
3191
|
-
});
|
|
3192
|
-
|
|
3193
|
-
it("stores prompt template versions with change notes and the cost budgets", async () => {
|
|
3194
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3195
|
-
const template: prompttemplate = {
|
|
3196
|
-
id: "t1",
|
|
3197
|
-
name: "pagesummary",
|
|
3198
|
-
body: "Summarize {{url}}.",
|
|
3199
|
-
variables: ["url"],
|
|
3200
|
-
version: 1,
|
|
3201
|
-
notes: "first version",
|
|
3202
|
-
createdat: now,
|
|
3203
|
-
};
|
|
3204
|
-
await store.setprompttemplates([template]);
|
|
3205
|
-
expect((await store.getprompttemplates())[0]?.notes).toBe("first version");
|
|
3206
|
-
const budget: costbudget = { maxtokens: 5000, configuredat: now };
|
|
3207
|
-
await store.setcostbudget(budget);
|
|
3208
|
-
expect((await store.getcostbudget())?.maxtokens).toBe(5000);
|
|
3209
|
-
await store.setcostbudget({ runid: "run1", maxcost: 1, currency: "usd", configuredat: now });
|
|
3210
|
-
expect((await store.getcostbudget("run1"))?.maxcost).toBe(1);
|
|
3211
|
-
expect((await store.getcostbudget("run2"))?.maxtokens).toBe(5000);
|
|
3212
|
-
});
|
|
3213
|
-
|
|
3214
|
-
it("stores the parsed command and the guard refusal notices with their reasons", async () => {
|
|
3215
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3216
|
-
const parse = {
|
|
3217
|
-
text: "open the docs",
|
|
3218
|
-
intent: "navigate" as const,
|
|
3219
|
-
entities: [{ name: "url", value: "https://docs.example" }],
|
|
3220
|
-
confidence: 0.9,
|
|
3221
|
-
model: "model-a",
|
|
3222
|
-
providerid: "prov1",
|
|
3223
|
-
parsedat: now,
|
|
3224
|
-
};
|
|
3225
|
-
await store.setcommandparse(parse);
|
|
3226
|
-
expect((await store.getcommandparse())?.intent).toBe("navigate");
|
|
3227
|
-
await store.addguardnotice({
|
|
3228
|
-
raw: "nope",
|
|
3229
|
-
verdict: "invalid",
|
|
3230
|
-
reason: "The model answer is not json.",
|
|
3231
|
-
attempts: 3,
|
|
3232
|
-
});
|
|
3233
|
-
await store.addguardnotice({ raw: "{}", parsed: {}, verdict: "valid", attempts: 1 });
|
|
3234
|
-
const notices = await store.getguardnotices();
|
|
3235
|
-
expect(notices).toHaveLength(1);
|
|
3236
|
-
expect(notices[0]?.reason).toMatch(/not json/i);
|
|
3237
|
-
});
|
|
3238
|
-
});
|
|
3239
|
-
|
|
3240
|
-
describe("sessionmemory swarm persistence", () => {
|
|
3241
|
-
const now = 1_800_000_000_000;
|
|
3242
|
-
it("stores the agent identities, the task queue and the blackboard as first class records", async () => {
|
|
3243
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3244
|
-
await store.setagents([
|
|
3245
|
-
{ id: "a1", name: "Scout", role: "worker", depth: 0, state: "active", tabid: 7, registeredat: now },
|
|
3246
|
-
]);
|
|
3247
|
-
expect((await store.getagents())[0]).toMatchObject({ name: "Scout", tabid: 7, role: "worker" });
|
|
3248
|
-
await store.settaskqueue({
|
|
3249
|
-
lanes: ["extraction"],
|
|
3250
|
-
priorities: [1],
|
|
3251
|
-
completionpolicy: "all",
|
|
3252
|
-
items: [
|
|
3253
|
-
{ id: "t1", lane: "extraction", priority: 1, payload: "Read the table", state: "queued", enqueuedat: now },
|
|
3254
|
-
],
|
|
3255
|
-
claims: [],
|
|
3256
|
-
});
|
|
3257
|
-
expect((await store.gettaskqueue())?.items).toHaveLength(1);
|
|
3258
|
-
await store.setblackboard({
|
|
3259
|
-
sections: ["goals", "facts", "findings", "scratch"],
|
|
3260
|
-
entries: [
|
|
3261
|
-
{
|
|
3262
|
-
id: "e1",
|
|
3263
|
-
key: "goal",
|
|
3264
|
-
valuekind: "text",
|
|
3265
|
-
value: "Extract",
|
|
3266
|
-
author: "user",
|
|
3267
|
-
section: "goals",
|
|
3268
|
-
consentclass: "read",
|
|
3269
|
-
postedat: now,
|
|
3270
|
-
},
|
|
3271
|
-
],
|
|
3272
|
-
});
|
|
3273
|
-
expect((await store.getblackboard())?.entries[0]?.key).toBe("goal");
|
|
3274
|
-
await store.setkillswitch({ engaged: true, engagedat: now, reason: "Halted." });
|
|
3275
|
-
expect((await store.getkillswitch())?.engaged).toBe(true);
|
|
3276
|
-
});
|
|
3277
|
-
|
|
3278
|
-
it("stores the lessons learned of the fleet with one row per lesson id", async () => {
|
|
3279
|
-
/* the lessons store of the 1.1.96 multi agent certification: a rewritten lesson replaces its row instead of duplicating it, so the store deduplicates and the reuse count survives the rewrite */
|
|
3280
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3281
|
-
const first: lessonrecord = {
|
|
3282
|
-
id: "l1",
|
|
3283
|
-
agentid: "w1",
|
|
3284
|
-
finding: "The pricing table loads only after the hero settles.",
|
|
3285
|
-
origin: "https://example.com",
|
|
3286
|
-
reusecount: 0,
|
|
3287
|
-
recordedat: now,
|
|
3288
|
-
};
|
|
3289
|
-
const second: lessonrecord = {
|
|
3290
|
-
id: "l2",
|
|
3291
|
-
agentid: "w2",
|
|
3292
|
-
finding: "The login form refuses pasted passwords.",
|
|
3293
|
-
origin: "https://shop.example",
|
|
3294
|
-
reusecount: 0,
|
|
3295
|
-
recordedat: now,
|
|
3296
|
-
};
|
|
3297
|
-
await store.setlesson(first);
|
|
3298
|
-
await store.setlesson(second);
|
|
3299
|
-
expect((await store.getlessons()).map((lesson) => lesson.id).sort()).toEqual(["l1", "l2"]);
|
|
3300
|
-
const rewritten: lessonrecord = { ...first, reusecount: 1, lastusedat: now + 1_000 };
|
|
3301
|
-
await store.setlesson(rewritten);
|
|
3302
|
-
const stored = await store.getlessons();
|
|
3303
|
-
expect(stored).toHaveLength(2);
|
|
3304
|
-
expect(stored.find((lesson) => lesson.id === "l1")).toMatchObject({ reusecount: 1, lastusedat: now + 1_000 });
|
|
3305
|
-
expect(stored.find((lesson) => lesson.id === "l1")?.finding).toBe(
|
|
3306
|
-
"The pricing table loads only after the hero settles.",
|
|
3307
|
-
);
|
|
3308
|
-
});
|
|
3309
|
-
|
|
3310
|
-
it("stores the mailbox contents with the user configured retention", async () => {
|
|
3311
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3312
|
-
await store.setmailboxes([
|
|
3313
|
-
{
|
|
3314
|
-
agentid: "a1",
|
|
3315
|
-
inbox: [
|
|
3316
|
-
{ id: "m1", senderid: "a2", recipient: "a1", routing: "direct", payload: "One", sentat: now },
|
|
3317
|
-
{ id: "m2", senderid: "a2", recipient: "a1", routing: "direct", payload: "Two", sentat: now + 1 },
|
|
3318
|
-
],
|
|
3319
|
-
outbox: [],
|
|
3320
|
-
unread: 2,
|
|
3321
|
-
},
|
|
3322
|
-
]);
|
|
3323
|
-
expect((await store.getmailboxes())[0]?.inbox).toHaveLength(2);
|
|
3324
|
-
await store.setsettings({ mailboxretention: 1 });
|
|
3325
|
-
await store.setmailboxes((await store.getmailboxes()).map((mailbox) => ({ ...mailbox })));
|
|
3326
|
-
expect((await store.getmailboxes())[0]?.inbox).toHaveLength(1);
|
|
3327
|
-
});
|
|
3328
|
-
|
|
3329
|
-
it("stores the spawn history, the per agent usage and the lifecycle events", async () => {
|
|
3330
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3331
|
-
await store.addspawnrecord({ id: "s1", parentid: "a1", childid: "a2", role: "planner", depth: 1, at: now });
|
|
3332
|
-
await store.addspawnrecord({ id: "s2", parentid: "a2", childid: "a3", role: "worker", depth: 2, at: now + 1 });
|
|
3333
|
-
expect((await store.getspawnrecords()).map((record) => record.childid)).toEqual(["a3", "a2"]);
|
|
3334
|
-
await store.setagentusage([{ agentid: "a1", tokens: 120, cost: 0.02, steps: 3, updatedat: now }]);
|
|
3335
|
-
expect((await store.getagentusage())[0]).toMatchObject({ tokens: 120, steps: 3 });
|
|
3336
|
-
await store.addagentevent({
|
|
3337
|
-
id: "e1",
|
|
3338
|
-
kind: "register",
|
|
3339
|
-
agentid: "a1",
|
|
3340
|
-
summary: "The user registered the agent Scout.",
|
|
3341
|
-
at: now,
|
|
3342
|
-
});
|
|
3343
|
-
expect((await store.getagentevents())[0]?.kind).toBe("register");
|
|
3344
|
-
});
|
|
3345
|
-
|
|
3346
|
-
it("returns the swarm overview at a glance from the stored records", async () => {
|
|
3347
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3348
|
-
await store.setagents([
|
|
3349
|
-
{ id: "a1", name: "Scout", role: "worker", depth: 0, state: "active", registeredat: now },
|
|
3350
|
-
{ id: "a2", name: "Scribe", role: "planner", depth: 0, state: "paused", registeredat: now },
|
|
3351
|
-
]);
|
|
3352
|
-
await store.settaskqueue({
|
|
3353
|
-
lanes: ["extraction"],
|
|
3354
|
-
priorities: [],
|
|
3355
|
-
completionpolicy: "all",
|
|
3356
|
-
items: [
|
|
3357
|
-
{ id: "t1", lane: "extraction", priority: 1, payload: "Read the table", state: "claimed", enqueuedat: now },
|
|
3358
|
-
{ id: "t2", lane: "extraction", priority: 1, payload: "Read the footer", state: "queued", enqueuedat: now },
|
|
3359
|
-
],
|
|
3360
|
-
claims: [{ agentid: "a1", taskid: "t1", claimedat: now, heartbeatat: now }],
|
|
3361
|
-
});
|
|
3362
|
-
await store.setmailboxes([
|
|
3363
|
-
{
|
|
3364
|
-
agentid: "a2",
|
|
3365
|
-
inbox: [{ id: "m1", senderid: "a1", recipient: "a2", routing: "direct", payload: "Hi.", sentat: now }],
|
|
3366
|
-
outbox: [],
|
|
3367
|
-
unread: 1,
|
|
3368
|
-
},
|
|
3369
|
-
]);
|
|
3370
|
-
const overview = await store.swarmoverview();
|
|
3371
|
-
expect(overview).toMatchObject({
|
|
3372
|
-
agents: 2,
|
|
3373
|
-
active: 1,
|
|
3374
|
-
paused: 1,
|
|
3375
|
-
tasks: 2,
|
|
3376
|
-
queued: 1,
|
|
3377
|
-
claimed: 1,
|
|
3378
|
-
messages: 1,
|
|
3379
|
-
unread: 1,
|
|
3380
|
-
});
|
|
3381
|
-
const empty = await new sessionmemory(new fakeadapter()).swarmoverview();
|
|
3382
|
-
expect(empty).toMatchObject({ agents: 0, tasks: 0, messages: 0 });
|
|
3383
|
-
});
|
|
3384
|
-
});
|
|
3385
|
-
|
|
3386
|
-
describe("sessionmemory orchestration persistence", () => {
|
|
3387
|
-
const now = 1_800_000_000_000;
|
|
3388
|
-
it("stores the topology, splits, reviews and verifier checks of the 1.1.59 family", async () => {
|
|
3389
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3390
|
-
await store.settopology({
|
|
3391
|
-
id: "top1",
|
|
3392
|
-
leaderid: "a1",
|
|
3393
|
-
workerids: ["a2"],
|
|
3394
|
-
criticids: ["a3"],
|
|
3395
|
-
verifierids: ["a4"],
|
|
3396
|
-
assignments: [{ workerid: "a2", taskid: "t1", slice: "Read the table", assignedat: now }],
|
|
3397
|
-
rule: { kind: "first" },
|
|
3398
|
-
electedat: now,
|
|
3399
|
-
});
|
|
3400
|
-
expect((await store.gettopology())?.leaderid).toBe("a1");
|
|
3401
|
-
await store.setplannersplits([
|
|
3402
|
-
{
|
|
3403
|
-
id: "s1",
|
|
3404
|
-
planownerid: "a1",
|
|
3405
|
-
runownerid: "a2",
|
|
3406
|
-
taskid: "t1",
|
|
3407
|
-
stepreports: [{ stepid: "step1", outcome: "done", detail: "Done.", reportedat: now }],
|
|
3408
|
-
splitat: now,
|
|
3409
|
-
},
|
|
3410
|
-
]);
|
|
3411
|
-
expect((await store.getplannersplits())[0]?.stepreports).toHaveLength(1);
|
|
3412
|
-
await store.addcriticreview({
|
|
3413
|
-
id: "r1",
|
|
3414
|
-
reviewerid: "a3",
|
|
3415
|
-
subjectagentid: "a2",
|
|
3416
|
-
verdict: "changes",
|
|
3417
|
-
issues: [],
|
|
3418
|
-
requiredchanges: ["Re-read the footer."],
|
|
3419
|
-
reviewedat: now,
|
|
3420
|
-
});
|
|
3421
|
-
expect((await store.getcriticreviews())[0]?.verdict).toBe("changes");
|
|
3422
|
-
await store.addverifiercheck({
|
|
3423
|
-
id: "v1",
|
|
3424
|
-
verifierid: "a4",
|
|
3425
|
-
claimagentid: "a2",
|
|
3426
|
-
claim: "The table holds 42 rows.",
|
|
3427
|
-
method: "re-read",
|
|
3428
|
-
outcome: "pass",
|
|
3429
|
-
checkedat: now,
|
|
3430
|
-
});
|
|
3431
|
-
expect((await store.getverifierchecks())[0]?.outcome).toBe("pass");
|
|
3432
|
-
await store.setreviewrequests([
|
|
3433
|
-
{
|
|
3434
|
-
id: "rq1",
|
|
3435
|
-
fromagentid: "a2",
|
|
3436
|
-
toagentid: "a3",
|
|
3437
|
-
subject: "The output",
|
|
3438
|
-
payload: "The rows",
|
|
3439
|
-
state: "open",
|
|
3440
|
-
requestedat: now,
|
|
3441
|
-
},
|
|
3442
|
-
]);
|
|
3443
|
-
expect((await store.getreviewrequests())[0]?.state).toBe("open");
|
|
3444
|
-
});
|
|
3445
|
-
|
|
3446
|
-
it("stores the handoff log with the resumed state, the locks and the conflict scans", async () => {
|
|
3447
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3448
|
-
await store.addhandoff({
|
|
3449
|
-
id: "h1",
|
|
3450
|
-
fromagentid: "a1",
|
|
3451
|
-
toagentid: "a2",
|
|
3452
|
-
tabid: 7,
|
|
3453
|
-
taskstate: "Halfway.",
|
|
3454
|
-
state: "prepared",
|
|
3455
|
-
createdat: now,
|
|
3456
|
-
});
|
|
3457
|
-
await store.updatehandoff({
|
|
3458
|
-
id: "h1",
|
|
3459
|
-
fromagentid: "a1",
|
|
3460
|
-
toagentid: "a2",
|
|
3461
|
-
tabid: 7,
|
|
3462
|
-
taskstate: "Halfway.",
|
|
3463
|
-
state: "resumed",
|
|
3464
|
-
createdat: now,
|
|
3465
|
-
transferredat: now + 1,
|
|
3466
|
-
resumedat: now + 2,
|
|
3467
|
-
});
|
|
3468
|
-
const handoffs = await store.gethandoffs();
|
|
3469
|
-
expect(handoffs).toHaveLength(1);
|
|
3470
|
-
expect(handoffs[0]).toMatchObject({ state: "resumed", resumedat: now + 2 });
|
|
3471
|
-
await store.setlocks([
|
|
3472
|
-
{
|
|
3473
|
-
key: "https://example.com|#form",
|
|
3474
|
-
holder: "a1",
|
|
3475
|
-
kind: "exclusive",
|
|
3476
|
-
origin: "https://example.com",
|
|
3477
|
-
selector: "#form",
|
|
3478
|
-
acquiredat: now,
|
|
3479
|
-
expiresat: now + 1000,
|
|
3480
|
-
},
|
|
3481
|
-
]);
|
|
3482
|
-
expect((await store.getlocks())[0]?.holder).toBe("a1");
|
|
3483
|
-
await store.addconflictscan({
|
|
3484
|
-
id: "s1",
|
|
3485
|
-
writers: [{ agentid: "a1", origin: "https://example.com", selector: "#form" }],
|
|
3486
|
-
overlaps: [],
|
|
3487
|
-
suggestedorder: [],
|
|
3488
|
-
clean: true,
|
|
3489
|
-
scannedat: now,
|
|
3490
|
-
});
|
|
3491
|
-
expect((await store.getconflictscans())[0]?.clean).toBe(true);
|
|
3492
|
-
});
|
|
3493
|
-
|
|
3494
|
-
it("stores the merged report with provenance, the board snapshots with retention, the escalations, the consensus rounds, the shared costs and the interleaved timeline", async () => {
|
|
3495
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3496
|
-
await store.setreport({
|
|
3497
|
-
id: "rep1",
|
|
3498
|
-
title: "The extraction",
|
|
3499
|
-
sections: [
|
|
3500
|
-
{
|
|
3501
|
-
title: "Task t1",
|
|
3502
|
-
entries: [{ id: "m1", agentid: "a1", taskid: "t1", key: "rowcount", value: "42", mergedat: now }],
|
|
3503
|
-
sources: ["a1"],
|
|
3504
|
-
},
|
|
3505
|
-
],
|
|
3506
|
-
sources: ["a1"],
|
|
3507
|
-
createdat: now,
|
|
3508
|
-
});
|
|
3509
|
-
expect((await store.getreport())?.sections[0]?.entries[0]?.agentid).toBe("a1");
|
|
3510
|
-
await store.addboardsnapshot({ id: "b1", builtat: now, lanes: [] });
|
|
3511
|
-
await store.addboardsnapshot({ id: "b2", builtat: now + 1, lanes: [] });
|
|
3512
|
-
expect((await store.getboardsnapshots()).map((board) => board.id)).toEqual(["b2", "b1"]);
|
|
3513
|
-
await store.setsettings({ boardretention: 1 });
|
|
3514
|
-
await store.addboardsnapshot({ id: "b3", builtat: now + 2, lanes: [] });
|
|
3515
|
-
expect(await store.getboardsnapshots()).toHaveLength(1);
|
|
3516
|
-
await store.addescalation({
|
|
3517
|
-
id: "e1",
|
|
3518
|
-
agentid: "a1",
|
|
3519
|
-
subject: "Which origin",
|
|
3520
|
-
context: "Both hold half.",
|
|
3521
|
-
state: "open",
|
|
3522
|
-
raisedat: now,
|
|
3523
|
-
});
|
|
3524
|
-
await store.updateescalation({
|
|
3525
|
-
id: "e1",
|
|
3526
|
-
agentid: "a1",
|
|
3527
|
-
subject: "Which origin",
|
|
3528
|
-
context: "Both hold half.",
|
|
3529
|
-
state: "decided",
|
|
3530
|
-
decision: "The second origin.",
|
|
3531
|
-
raisedat: now,
|
|
3532
|
-
decidedat: now + 1,
|
|
3533
|
-
});
|
|
3534
|
-
expect((await store.getescalations())[0]?.decision).toBe("The second origin.");
|
|
3535
|
-
await store.setconsensusround({
|
|
3536
|
-
id: "c1",
|
|
3537
|
-
subject: "The lane order",
|
|
3538
|
-
votes: [],
|
|
3539
|
-
quorum: 2,
|
|
3540
|
-
state: "open",
|
|
3541
|
-
openedat: now,
|
|
3542
|
-
});
|
|
3543
|
-
await store.setconsensusround({
|
|
3544
|
-
id: "c1",
|
|
3545
|
-
subject: "The lane order",
|
|
3546
|
-
votes: [{ agentid: "a1", vote: "yes", votedat: now + 1 }],
|
|
3547
|
-
quorum: 2,
|
|
3548
|
-
state: "open",
|
|
3549
|
-
openedat: now,
|
|
3550
|
-
});
|
|
3551
|
-
const rounds = await store.getconsensusrounds();
|
|
3552
|
-
expect(rounds).toHaveLength(1);
|
|
3553
|
-
expect(rounds[0]?.votes).toHaveLength(1);
|
|
3554
|
-
await store.setconsensusround({ id: "c2", subject: "Abort", votes: [], quorum: 1, state: "open", openedat: now });
|
|
3555
|
-
expect(await store.getconsensusrounds()).toHaveLength(2);
|
|
3556
|
-
await store.addswarmcost({ agents: 2, tokens: 150, cost: 0.75, steps: 5, computedat: now });
|
|
3557
|
-
expect((await store.getswarmcosts())[0]).toMatchObject({ tokens: 150, steps: 5 });
|
|
3558
|
-
await store.addswarmaction({ id: "t1", kind: "claim", agentid: "a1", summary: "The claim.", at: now });
|
|
3559
|
-
await store.addswarmaction({ id: "t2", kind: "lock", agentid: "a2", summary: "The lock.", at: now + 1 });
|
|
3560
|
-
await store.addswarmaction({ id: "t3", kind: "merge", summary: "The merge.", at: now + 2 });
|
|
3561
|
-
expect((await store.getswarmtimeline()).map((action) => action.id)).toEqual(["t1", "t2", "t3"]);
|
|
3562
|
-
expect((await store.getswarmtimeline({ agentid: "a1" })).map((action) => action.id)).toEqual(["t1"]);
|
|
3563
|
-
expect((await store.getswarmtimeline({ kind: "lock" })).map((action) => action.id)).toEqual(["t2"]);
|
|
3564
|
-
expect((await store.getswarmtimeline({ since: now + 1 })).map((action) => action.id)).toEqual(["t2", "t3"]);
|
|
3565
|
-
});
|
|
3566
|
-
});
|
|
3567
|
-
|
|
3568
|
-
describe("execution environment persistence", () => {
|
|
3569
|
-
it("joins the environment grants to the session record and reads them back", async () => {
|
|
3570
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3571
|
-
await store.setsession({ id: "s1", tabid: 1, origin: "https://example.com", startedat: 1, expiresat: 2 });
|
|
3572
|
-
expect(await store.getenvironmentgrants()).toBeUndefined();
|
|
3573
|
-
await store.setenvironmentgrants(["pagecontext", "isolatedworld"]);
|
|
3574
|
-
expect(await store.getenvironmentgrants()).toEqual(["pagecontext", "isolatedworld"]);
|
|
3575
|
-
expect((await store.getsession())?.environmentgrants).toEqual(["pagecontext", "isolatedworld"]);
|
|
3576
|
-
await expect(store.setenvironmentgrants([])).resolves.toBeUndefined();
|
|
3577
|
-
});
|
|
3578
|
-
|
|
3579
|
-
it("seals the run state per profile so parallel profiles never share it and tampered seals never load", async () => {
|
|
3580
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3581
|
-
const state = {
|
|
3582
|
-
runid: "run1",
|
|
3583
|
-
sessionid: "s1",
|
|
3584
|
-
planid: "p1",
|
|
3585
|
-
profileid: "profile1",
|
|
3586
|
-
state: "active" as const,
|
|
3587
|
-
urlhistory: [],
|
|
3588
|
-
environments: {},
|
|
3589
|
-
turnarounds: {},
|
|
3590
|
-
keepalive: {
|
|
3591
|
-
runid: "run1",
|
|
3592
|
-
sessionid: "s1",
|
|
3593
|
-
state: "active" as const,
|
|
3594
|
-
startedat: 1,
|
|
3595
|
-
interval: 30_000,
|
|
3596
|
-
beats: 1,
|
|
3597
|
-
lastbeatat: 2,
|
|
3598
|
-
portopen: true,
|
|
3599
|
-
events: [],
|
|
3600
|
-
},
|
|
3601
|
-
updatedat: 2,
|
|
3602
|
-
};
|
|
3603
|
-
await store.setrunstate("profile1", state);
|
|
3604
|
-
await store.setrunstate("profile2", { ...state, profileid: "profile2", runid: "run2" });
|
|
3605
|
-
expect((await store.getrunstate("profile1"))?.runid).toBe("run1");
|
|
3606
|
-
expect((await store.getrunstate("profile2"))?.runid).toBe("run2");
|
|
3607
|
-
expect((await store.listrunstates()).map((entry) => entry.runid).sort()).toEqual(["run1", "run2"]);
|
|
3608
|
-
await store.removerunstate("profile2");
|
|
3609
|
-
expect(await store.getrunstate("profile2")).toBeUndefined();
|
|
3610
|
-
expect((await store.listrunstates()).map((entry) => entry.runid)).toEqual(["run1"]);
|
|
3611
|
-
});
|
|
3612
|
-
|
|
3613
|
-
it("expires the stale stopped run states past the user configured window while the keepalive summaries survive", async () => {
|
|
3614
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3615
|
-
const stopped = {
|
|
3616
|
-
runid: "run1",
|
|
3617
|
-
sessionid: "s1",
|
|
3618
|
-
planid: "p1",
|
|
3619
|
-
profileid: "default",
|
|
3620
|
-
state: "completed" as const,
|
|
3621
|
-
urlhistory: [{ url: "https://example.com/a", stepid: "s1", at: 1 }],
|
|
3622
|
-
environments: { s1: "pagecontext" as const },
|
|
3623
|
-
turnarounds: {},
|
|
3624
|
-
keepalive: {
|
|
3625
|
-
runid: "run1",
|
|
3626
|
-
sessionid: "s1",
|
|
3627
|
-
state: "stopped" as const,
|
|
3628
|
-
startedat: 1,
|
|
3629
|
-
interval: 30_000,
|
|
3630
|
-
beats: 4,
|
|
3631
|
-
lastbeatat: 2,
|
|
3632
|
-
portopen: false,
|
|
3633
|
-
events: [],
|
|
3634
|
-
stoppedat: 3,
|
|
3635
|
-
},
|
|
3636
|
-
updatedat: 3,
|
|
3637
|
-
};
|
|
3638
|
-
const active = {
|
|
3639
|
-
...stopped,
|
|
3640
|
-
runid: "run2",
|
|
3641
|
-
keepalive: { ...stopped.keepalive, runid: "run2", state: "active" as const, portopen: true },
|
|
3642
|
-
updatedat: 100,
|
|
3643
|
-
};
|
|
3644
|
-
await store.setrunstate("default", stopped);
|
|
3645
|
-
await store.setrunstate("live", active);
|
|
3646
|
-
const kept = await store.expirerunstates(1_000, 5_000);
|
|
3647
|
-
expect(kept.map((entry) => entry.runid)).toEqual(["run2"]);
|
|
3648
|
-
const summary = await store.getrunstate("default");
|
|
3649
|
-
expect(summary?.state).toBe("expired");
|
|
3650
|
-
expect(summary?.urlhistory).toEqual([]);
|
|
3651
|
-
expect(summary?.keepalive.beats).toBe(4);
|
|
3652
|
-
expect(await store.expirerunstates(undefined, 9_000)).toHaveLength(2);
|
|
3653
|
-
});
|
|
3654
|
-
|
|
3655
|
-
it("stores worker events with provenance, the offscreen registry, sandbox renders and the run locks", async () => {
|
|
3656
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3657
|
-
await store.addworkerevent({
|
|
3658
|
-
id: "w1",
|
|
3659
|
-
runid: "run1",
|
|
3660
|
-
kind: "spawn",
|
|
3661
|
-
workers: 2,
|
|
3662
|
-
reason: "Two parses waited.",
|
|
3663
|
-
provenance: { origin: "https://example.com", stepid: "s1", environment: "offscreenworker" },
|
|
3664
|
-
at: 1,
|
|
3665
|
-
});
|
|
3666
|
-
await store.addworkerevent({
|
|
3667
|
-
id: "w2",
|
|
3668
|
-
runid: "run1",
|
|
3669
|
-
kind: "teardown",
|
|
3670
|
-
workers: 0,
|
|
3671
|
-
reason: "The run completed.",
|
|
3672
|
-
provenance: { origin: "https://example.com", stepid: "s1", environment: "offscreenworker" },
|
|
3673
|
-
at: 2,
|
|
3674
|
-
});
|
|
3675
|
-
expect((await store.getworkerevents()).map((event) => event.kind)).toEqual(["teardown", "spawn"]);
|
|
3676
|
-
await store.addoffscreenentry({
|
|
3677
|
-
document: "offscreen.html",
|
|
3678
|
-
runid: "run1",
|
|
3679
|
-
reasons: ["DOM_PARSER", "WORKERS"],
|
|
3680
|
-
justification: "Heavy parsing of reviewed snapshots.",
|
|
3681
|
-
createdat: 1,
|
|
3682
|
-
});
|
|
3683
|
-
await store.updateoffscreenentry({
|
|
3684
|
-
document: "offscreen.html",
|
|
3685
|
-
runid: "run1",
|
|
3686
|
-
reasons: ["DOM_PARSER", "WORKERS"],
|
|
3687
|
-
justification: "Heavy parsing of reviewed snapshots.",
|
|
3688
|
-
createdat: 1,
|
|
3689
|
-
closedat: 9,
|
|
3690
|
-
});
|
|
3691
|
-
expect((await store.getoffscreenentries())[0]?.closedat).toBe(9);
|
|
3692
|
-
await store.addsandboxrender({
|
|
3693
|
-
id: "r1",
|
|
3694
|
-
nonce: "abc123def456ghi7",
|
|
3695
|
-
markup: "<p>ok</p>",
|
|
3696
|
-
sourceorigin: "https://example.com",
|
|
3697
|
-
stepid: "s1",
|
|
3698
|
-
renderedat: 1,
|
|
3699
|
-
});
|
|
3700
|
-
expect((await store.getsandboxrenders())[0]?.nonce).toBe("abc123def456ghi7");
|
|
3701
|
-
await store.setrunlocks([{ sessionid: "s1", runid: "run1", holder: "executor", acquiredat: 1 }]);
|
|
3702
|
-
expect((await store.getrunlocks())[0]?.runid).toBe("run1");
|
|
3703
|
-
});
|
|
3704
|
-
|
|
3705
|
-
it("tracks the storage quota usage of the run state and exports every run state as one record", async () => {
|
|
3706
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3707
|
-
await store.setsettings({ runstatebytes: 500 });
|
|
3708
|
-
await store.trackrunstatequota(1_200);
|
|
3709
|
-
const quota = await store.getrunstatequota();
|
|
3710
|
-
expect(quota).toMatchObject({ used: 1_200, ceiling: 500 });
|
|
3711
|
-
const state = {
|
|
3712
|
-
runid: "run1",
|
|
3713
|
-
sessionid: "s1",
|
|
3714
|
-
planid: "p1",
|
|
3715
|
-
profileid: "default",
|
|
3716
|
-
state: "active" as const,
|
|
3717
|
-
urlhistory: [],
|
|
3718
|
-
environments: { s1: "offscreenworker" as const },
|
|
3719
|
-
turnarounds: { s1: 8 },
|
|
3720
|
-
keepalive: {
|
|
3721
|
-
runid: "run1",
|
|
3722
|
-
sessionid: "s1",
|
|
3723
|
-
state: "active" as const,
|
|
3724
|
-
startedat: 1,
|
|
3725
|
-
interval: 30_000,
|
|
3726
|
-
beats: 2,
|
|
3727
|
-
lastbeatat: 2,
|
|
3728
|
-
portopen: true,
|
|
3729
|
-
events: [],
|
|
3730
|
-
},
|
|
3731
|
-
updatedat: 2,
|
|
3732
|
-
};
|
|
3733
|
-
await store.setrunstate("default", state);
|
|
3734
|
-
const exported = await store.exportrunstates();
|
|
3735
|
-
expect(exported).toMatchObject({ runs: 1, environments: 1, offloaded: 1, beats: 2 });
|
|
3736
|
-
});
|
|
3737
|
-
|
|
3738
|
-
it("stores the automation allowlist, the origin profiles, the consent windows and the class consents of the security family", async () => {
|
|
3739
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3740
|
-
await store.addallowlistorigin({ origin: "https://example.com", profileid: "default", grantedat: 1 });
|
|
3741
|
-
await store.addallowlistorigin({ origin: "https://example.com", profileid: "default", grantedat: 2 });
|
|
3742
|
-
await store.addallowlistorigin({ origin: "https://other.example", profileid: "default", grantedat: 3 });
|
|
3743
|
-
expect((await store.getautomationallowlist()).map((entry) => entry.origin)).toEqual([
|
|
3744
|
-
"https://example.com",
|
|
3745
|
-
"https://other.example",
|
|
3746
|
-
]);
|
|
3747
|
-
await store.removeallowlistorigin("https://other.example", "default");
|
|
3748
|
-
expect(await store.getautomationallowlist()).toHaveLength(1);
|
|
3749
|
-
await store.saveoriginprofile({
|
|
3750
|
-
profileid: "p1",
|
|
3751
|
-
origin: "https://example.com",
|
|
3752
|
-
grants: ["fillcard" as const],
|
|
3753
|
-
denials: [],
|
|
3754
|
-
createdat: 1,
|
|
3755
|
-
updatedat: 1,
|
|
3756
|
-
});
|
|
3757
|
-
await store.saveoriginprofile({
|
|
3758
|
-
profileid: "p1",
|
|
3759
|
-
origin: "https://example.com",
|
|
3760
|
-
grants: [],
|
|
3761
|
-
denials: ["fillcard" as const],
|
|
3762
|
-
createdat: 1,
|
|
3763
|
-
updatedat: 2,
|
|
3764
|
-
});
|
|
3765
|
-
await store.saveoriginprofile({
|
|
3766
|
-
profileid: "p2",
|
|
3767
|
-
origin: "https://other.example",
|
|
3768
|
-
grants: [],
|
|
3769
|
-
denials: [],
|
|
3770
|
-
createdat: 1,
|
|
3771
|
-
updatedat: 1,
|
|
3772
|
-
});
|
|
3773
|
-
const profiles = await store.getoriginprofiles();
|
|
3774
|
-
expect(profiles).toHaveLength(2);
|
|
3775
|
-
expect(profiles.find((profile) => profile.origin === "https://example.com")?.denials).toEqual(["fillcard"]);
|
|
3776
|
-
await store.setconsentwindows([
|
|
3777
|
-
{
|
|
3778
|
-
id: "w1",
|
|
3779
|
-
sessionid: "s1",
|
|
3780
|
-
origin: "https://example.com",
|
|
3781
|
-
startedat: 1,
|
|
3782
|
-
duration: 10,
|
|
3783
|
-
expiresat: 11,
|
|
3784
|
-
boundary: "10 milliseconds",
|
|
3785
|
-
kinds: [],
|
|
3786
|
-
state: "active",
|
|
3787
|
-
},
|
|
3788
|
-
]);
|
|
3789
|
-
const expired = await store.expireconsentwindows(12);
|
|
3790
|
-
expect(expired[0]?.state).toBe("closed");
|
|
3791
|
-
expect(expired[0]?.closedat).toBe(12);
|
|
3792
|
-
await store.addclassconsent({ id: "c1", origin: "https://example.com", sensitiveclass: "payment", grantedat: 1 });
|
|
3793
|
-
await store.addclassconsent({ id: "c2", origin: "https://example.com", sensitiveclass: "payment", grantedat: 2 });
|
|
3794
|
-
expect((await store.getclassconsents()).filter((consent) => consent.sensitiveclass === "payment")).toHaveLength(1);
|
|
3795
|
-
await store.addrevocation({
|
|
3796
|
-
id: "r1",
|
|
3797
|
-
sessionid: "s1",
|
|
3798
|
-
runid: "run1",
|
|
3799
|
-
haltedstepids: ["s1", "s2"],
|
|
3800
|
-
actor: "user",
|
|
3801
|
-
reason: "The user revoked the consent mid run.",
|
|
3802
|
-
at: 3,
|
|
3803
|
-
});
|
|
3804
|
-
expect((await store.getrevocations())[0]?.haltedstepids).toEqual(["s1", "s2"]);
|
|
3805
|
-
});
|
|
3806
|
-
|
|
3807
|
-
it("stores the mask rules per origin with add and remove", async () => {
|
|
3808
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3809
|
-
await store.addmaskrule({ id: "m1", shapes: ["iban"], createdat: 1 });
|
|
3810
|
-
await store.addmaskrule({ id: "m2", origin: "https://example.com", shapes: ["routing"], createdat: 2 });
|
|
3811
|
-
expect((await store.getmaskrules()).map((rule) => rule.id)).toEqual(["m1", "m2"]);
|
|
3812
|
-
await store.removemaskrule("m1");
|
|
3813
|
-
expect((await store.getmaskrules()).map((rule) => rule.id)).toEqual(["m2"]);
|
|
3814
|
-
});
|
|
3815
|
-
|
|
3816
|
-
it("stores the immutable run log and exports only the verified chain", async () => {
|
|
3817
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3818
|
-
let log = { runid: "run1", sessionid: "s1", entries: [] as never[], updatedat: 1 } as Parameters<
|
|
3819
|
-
sessionmemory["setimmutablelog"]
|
|
3820
|
-
>[0];
|
|
3821
|
-
const { appendlogentry, sealrunlog } = await import("../security.js");
|
|
3822
|
-
log = await appendlogentry({
|
|
3823
|
-
log,
|
|
3824
|
-
kind: "grant",
|
|
3825
|
-
summary: "The session started.",
|
|
3826
|
-
origin: "https://example.com",
|
|
3827
|
-
at: 2,
|
|
3828
|
-
});
|
|
3829
|
-
log = await appendlogentry({
|
|
3830
|
-
log,
|
|
3831
|
-
kind: "step",
|
|
3832
|
-
summary: "The click step completed.",
|
|
3833
|
-
origin: "https://example.com",
|
|
3834
|
-
stepid: "s1",
|
|
3835
|
-
at: 3,
|
|
3836
|
-
});
|
|
3837
|
-
await store.trackimmutablelog("run1");
|
|
3838
|
-
await store.setimmutablelog(log);
|
|
3839
|
-
const stored = await store.getimmutablelog("run1");
|
|
3840
|
-
expect(stored?.entries).toHaveLength(2);
|
|
3841
|
-
expect(await store.listimmutablelogs()).toHaveLength(1);
|
|
3842
|
-
const sealed = await sealrunlog(log, 4);
|
|
3843
|
-
await store.setimmutablelog(sealed.log);
|
|
3844
|
-
const exported = await store.exportverifiedrunlog("run1");
|
|
3845
|
-
expect(exported.chainvalid).toBe(true);
|
|
3846
|
-
expect(exported.entries).toBe(2);
|
|
3847
|
-
expect(exported.sealhash).toMatch(/^[0-9a-f]{64}$/);
|
|
3848
|
-
await expect(store.exportverifiedrunlog("missing")).rejects.toThrow(/no run log exists/i);
|
|
3849
|
-
const expired = await store.expireimmutablelogs(10, 100);
|
|
3850
|
-
expect(expired).toHaveLength(0);
|
|
3851
|
-
const pruned = await store.getimmutablelog("run1");
|
|
3852
|
-
expect(pruned?.entries).toEqual([]);
|
|
3853
|
-
expect(pruned?.seal?.sealhash.current).toMatch(/^[0-9a-f]{64}$/);
|
|
3854
|
-
});
|
|
3855
|
-
});
|
|
3856
|
-
|
|
3857
|
-
describe("sessionmemory security part two", () => {
|
|
3858
|
-
it("stores secretvault metadata with labels and scopes only and never values", async () => {
|
|
3859
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3860
|
-
const entry = {
|
|
3861
|
-
vaultid: "v1",
|
|
3862
|
-
label: "Bank login",
|
|
3863
|
-
scope: "https://bank.example",
|
|
3864
|
-
profileid: "profile",
|
|
3865
|
-
provenance: "user" as const,
|
|
3866
|
-
algorithm: "sha-256" as const,
|
|
3867
|
-
digest: "sha256:abc",
|
|
3868
|
-
createdat: 1,
|
|
3869
|
-
};
|
|
3870
|
-
await store.addsecret(entry);
|
|
3871
|
-
await store.addsecret({ ...entry, label: "duplicate id keeps first" });
|
|
3872
|
-
expect(await store.getsecretvault()).toHaveLength(1);
|
|
3873
|
-
await store.stampsecretuse("v1", 9);
|
|
3874
|
-
expect((await store.getsecretvault())[0]?.lastusedat).toBe(9);
|
|
3875
|
-
await store.removesecret("v1");
|
|
3876
|
-
expect(await store.getsecretvault()).toEqual([]);
|
|
3877
|
-
});
|
|
3878
|
-
|
|
3879
|
-
it("stores connectallow entries with their senders and drops them on removal", async () => {
|
|
3880
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3881
|
-
await store.addconnectallow({ senderid: "sender", displayname: "Bridge", addedat: 1 });
|
|
3882
|
-
await store.addconnectallow({ senderid: "sender", displayname: "duplicate keeps first", addedat: 2 });
|
|
3883
|
-
expect((await store.getconnectallow())[0]?.displayname).toBe("Bridge");
|
|
3884
|
-
await store.removeconnectallow("sender");
|
|
3885
|
-
expect(await store.getconnectallow()).toEqual([]);
|
|
3886
|
-
});
|
|
3887
|
-
|
|
3888
|
-
it("stores ratelimit bucket state per origin and per session", async () => {
|
|
3889
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3890
|
-
await store.saveratelimitbucket({
|
|
3891
|
-
origin: "https://example.com",
|
|
3892
|
-
sessionid: "s",
|
|
3893
|
-
limit: 5,
|
|
3894
|
-
window: 1000,
|
|
3895
|
-
used: 1,
|
|
3896
|
-
windowstartedat: 1,
|
|
3897
|
-
resetsat: 1001,
|
|
3898
|
-
});
|
|
3899
|
-
await store.saveratelimitbucket({
|
|
3900
|
-
origin: "https://example.com",
|
|
3901
|
-
sessionid: "s",
|
|
3902
|
-
limit: 5,
|
|
3903
|
-
window: 1000,
|
|
3904
|
-
used: 2,
|
|
3905
|
-
windowstartedat: 1,
|
|
3906
|
-
resetsat: 1001,
|
|
3907
|
-
});
|
|
3908
|
-
expect(await store.getratelimitbuckets()).toHaveLength(1);
|
|
3909
|
-
await store.saveratelimitbucket({
|
|
3910
|
-
origin: "https://other.example",
|
|
3911
|
-
sessionid: "s",
|
|
3912
|
-
limit: 5,
|
|
3913
|
-
window: 1000,
|
|
3914
|
-
used: 0,
|
|
3915
|
-
windowstartedat: 1,
|
|
3916
|
-
resetsat: 1001,
|
|
3917
|
-
});
|
|
3918
|
-
expect(await store.getratelimitbuckets()).toHaveLength(2);
|
|
3919
|
-
await store.removeratelimitbucket("https://example.com", "s");
|
|
3920
|
-
expect((await store.getratelimitbuckets()).map((bucket) => bucket.origin)).toEqual(["https://other.example"]);
|
|
3921
|
-
});
|
|
3922
|
-
|
|
3923
|
-
it("stores confirm gates with their resolution events and human action provenance", async () => {
|
|
3924
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3925
|
-
const gate = {
|
|
3926
|
-
gateid: "g1",
|
|
3927
|
-
kind: "confirmpay" as const,
|
|
3928
|
-
stepid: "step",
|
|
3929
|
-
runid: "run",
|
|
3930
|
-
origin: "https://shop.example",
|
|
3931
|
-
payload: { amount: "10.00" },
|
|
3932
|
-
state: "open" as const,
|
|
3933
|
-
openedat: 1,
|
|
3934
|
-
};
|
|
3935
|
-
await store.savegate(gate);
|
|
3936
|
-
await store.savegate({ ...gate, state: "resolved", resolvedat: 3, actor: "user" });
|
|
3937
|
-
expect((await store.getgates())[0]?.state).toBe("resolved");
|
|
3938
|
-
await store.savegate({ ...gate, gateid: "g2", kind: "confirmdelete" as const });
|
|
3939
|
-
expect((await store.getgates()).map((candidate) => candidate.gateid)).toEqual(["g2", "g1"]);
|
|
3940
|
-
await store.addgateresolution({
|
|
3941
|
-
gateid: "g1",
|
|
3942
|
-
kind: "confirmpay",
|
|
3943
|
-
stepid: "step",
|
|
3944
|
-
decision: "resolved",
|
|
3945
|
-
actor: "user",
|
|
3946
|
-
at: 3,
|
|
3947
|
-
});
|
|
3948
|
-
expect((await store.getgateresolutions())[0]).toMatchObject({ gateid: "g1", actor: "user" });
|
|
3949
|
-
});
|
|
3950
|
-
|
|
3951
|
-
it("stores redactshot regions per origin and page template", async () => {
|
|
3952
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3953
|
-
const region = {
|
|
3954
|
-
id: "r1",
|
|
3955
|
-
origin: "https://bank.example",
|
|
3956
|
-
template: "login",
|
|
3957
|
-
x: 0,
|
|
3958
|
-
y: 0,
|
|
3959
|
-
width: 10,
|
|
3960
|
-
height: 10,
|
|
3961
|
-
reason: "the card field",
|
|
3962
|
-
source: "userdrawn" as const,
|
|
3963
|
-
createdat: 1,
|
|
3964
|
-
};
|
|
3965
|
-
await store.addredactregion(region);
|
|
3966
|
-
expect(await store.getredactregions()).toHaveLength(1);
|
|
3967
|
-
await store.removeredactregion("r1");
|
|
3968
|
-
expect(await store.getredactregions()).toEqual([]);
|
|
3969
|
-
});
|
|
3970
|
-
|
|
3971
|
-
it("stores phishguard verdicts and expires the ones past the freshness window", async () => {
|
|
3972
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3973
|
-
await store.addphishverdict({
|
|
3974
|
-
origin: "https://pay.example.com",
|
|
3975
|
-
matchedorigin: "https://pay.example.com",
|
|
3976
|
-
distance: 0.2,
|
|
3977
|
-
threshold: 0.4,
|
|
3978
|
-
blocked: false,
|
|
3979
|
-
reason: "under the threshold",
|
|
3980
|
-
at: 1,
|
|
3981
|
-
});
|
|
3982
|
-
await store.addphishverdict({
|
|
3983
|
-
origin: "https://pay.example.com",
|
|
3984
|
-
distance: 0.3,
|
|
3985
|
-
threshold: 0.4,
|
|
3986
|
-
blocked: true,
|
|
3987
|
-
reason: "crossed the threshold",
|
|
3988
|
-
at: 5,
|
|
3989
|
-
});
|
|
3990
|
-
expect(await store.getphishverdicts()).toHaveLength(1);
|
|
3991
|
-
expect((await store.getphishverdicts())[0]?.at).toBe(5);
|
|
3992
|
-
expect(await store.expirephishverdicts(undefined, 100)).toHaveLength(1);
|
|
3993
|
-
expect(await store.expirephishverdicts(10, 100)).toEqual([]);
|
|
3994
|
-
});
|
|
3995
|
-
|
|
3996
|
-
it("stores permdiff records of each installed version with the last permission set", async () => {
|
|
3997
|
-
const store = new sessionmemory(new fakeadapter());
|
|
3998
|
-
await store.addpermdiff({
|
|
3999
|
-
fromversion: "1.1.61",
|
|
4000
|
-
toversion: "1.1.62",
|
|
4001
|
-
added: ["optional:downloads"],
|
|
4002
|
-
removed: [],
|
|
4003
|
-
computedat: 1,
|
|
4004
|
-
});
|
|
4005
|
-
expect((await store.getpermdiffs())[0]?.added).toEqual(["optional:downloads"]);
|
|
4006
|
-
await store.setlastpermissions(["required:storage"], "1.1.62");
|
|
4007
|
-
expect(await store.getlastpermissions()).toEqual({ permissions: ["required:storage"], version: "1.1.62" });
|
|
4008
|
-
});
|
|
4009
|
-
|
|
4010
|
-
it("records safedefaults applications with their first seen origins and deferred command events", async () => {
|
|
4011
|
-
const store = new sessionmemory(new fakeadapter());
|
|
4012
|
-
await store.addsafedefaultapplication({ origin: "https://first.example", firstseenat: 1 });
|
|
4013
|
-
await store.addsafedefaultapplication({ origin: "https://first.example", firstseenat: 2 });
|
|
4014
|
-
expect(await store.getsafedefaultapplications()).toEqual([{ origin: "https://first.example", firstseenat: 1 }]);
|
|
4015
|
-
await store.adddeferredevent({
|
|
4016
|
-
id: "d1",
|
|
4017
|
-
stepid: "step",
|
|
4018
|
-
kind: "click",
|
|
4019
|
-
origin: "https://example.com",
|
|
4020
|
-
reason: "bucket full",
|
|
4021
|
-
resetsat: 10,
|
|
4022
|
-
at: 1,
|
|
4023
|
-
});
|
|
4024
|
-
expect((await store.getdeferredevents())[0]?.resetsat).toBe(10);
|
|
4025
|
-
});
|
|
4026
|
-
|
|
4027
|
-
it("serves the transparency data of the transparencypage in one read", async () => {
|
|
4028
|
-
const store = new sessionmemory(new fakeadapter());
|
|
4029
|
-
await store.addallowlistorigin({ origin: "https://example.com", profileid: "profile", grantedat: 1 });
|
|
4030
|
-
await store.addconnectallow({ senderid: "sender", displayname: "Bridge", addedat: 1 });
|
|
4031
|
-
await store.addsecret({
|
|
4032
|
-
vaultid: "v1",
|
|
4033
|
-
label: "Bank login",
|
|
4034
|
-
scope: "https://bank.example",
|
|
4035
|
-
profileid: "profile",
|
|
4036
|
-
provenance: "user",
|
|
4037
|
-
algorithm: "sha-256",
|
|
4038
|
-
digest: "sha256:abc",
|
|
4039
|
-
createdat: 1,
|
|
4040
|
-
});
|
|
4041
|
-
await store.addpermdiff({ fromversion: "1.1.61", toversion: "1.1.62", added: [], removed: [], computedat: 1 });
|
|
4042
|
-
await store.addsafedefaultapplication({ origin: "https://first.example", firstseenat: 1 });
|
|
4043
|
-
await store.savegate({
|
|
4044
|
-
gateid: "g1",
|
|
4045
|
-
kind: "confirmpay",
|
|
4046
|
-
stepid: "step",
|
|
4047
|
-
runid: "run",
|
|
4048
|
-
origin: "https://shop.example",
|
|
4049
|
-
payload: {},
|
|
4050
|
-
state: "open",
|
|
4051
|
-
openedat: 1,
|
|
4052
|
-
});
|
|
4053
|
-
await store.adddeferredevent({
|
|
4054
|
-
id: "d1",
|
|
4055
|
-
stepid: "step",
|
|
4056
|
-
kind: "click",
|
|
4057
|
-
origin: "https://example.com",
|
|
4058
|
-
reason: "bucket full",
|
|
4059
|
-
resetsat: 10,
|
|
4060
|
-
at: 1,
|
|
4061
|
-
});
|
|
4062
|
-
await store.addphishverdict({
|
|
4063
|
-
origin: "https://pay.example.com",
|
|
4064
|
-
distance: 0.2,
|
|
4065
|
-
threshold: 0.4,
|
|
4066
|
-
blocked: false,
|
|
4067
|
-
reason: "under",
|
|
4068
|
-
at: 1,
|
|
4069
|
-
});
|
|
4070
|
-
const view = await store.gettransparencyview();
|
|
4071
|
-
expect(view.allowlist).toHaveLength(1);
|
|
4072
|
-
expect(view.profiles).toEqual([]);
|
|
4073
|
-
expect(view.connectallow).toHaveLength(1);
|
|
4074
|
-
expect(view.permdiffs).toHaveLength(1);
|
|
4075
|
-
expect(view.safedefaults).toHaveLength(1);
|
|
4076
|
-
expect(view.vault[0]?.label).toBe("Bank login");
|
|
4077
|
-
expect(view.gates[0]?.gateid).toBe("g1");
|
|
4078
|
-
expect(view.resolutions).toEqual([]);
|
|
4079
|
-
expect(view.deferred).toHaveLength(1);
|
|
4080
|
-
expect(view.phishverdicts).toHaveLength(1);
|
|
4081
|
-
});
|
|
4082
|
-
});
|
|
4083
|
-
|
|
4084
|
-
/* ── The 1.1.95 purge and export completeness of the stored families. ── */
|
|
4085
|
-
|
|
4086
|
-
describe("sessionmemory purge and export completeness", () => {
|
|
4087
|
-
it("lists the stored keys of every run trace and session trail the families wrote", async () => {
|
|
4088
|
-
const store = new sessionmemory(new fakeadapter());
|
|
4089
|
-
await store.setrunrecord({
|
|
4090
|
-
runid: "run-one",
|
|
4091
|
-
planid: "plan",
|
|
4092
|
-
sessionid: "session",
|
|
4093
|
-
state: "completed",
|
|
4094
|
-
createdat: 1,
|
|
4095
|
-
updatedat: 2,
|
|
4096
|
-
});
|
|
4097
|
-
await store.setrunrecord({
|
|
4098
|
-
runid: "run-two",
|
|
4099
|
-
planid: "plan",
|
|
4100
|
-
sessionid: "session",
|
|
4101
|
-
state: "running",
|
|
4102
|
-
createdat: 3,
|
|
4103
|
-
updatedat: 4,
|
|
4104
|
-
});
|
|
4105
|
-
await store.addsessionrecord({
|
|
4106
|
-
id: "session",
|
|
4107
|
-
name: "workspace",
|
|
4108
|
-
createdat: 1,
|
|
4109
|
-
tabs: [],
|
|
4110
|
-
captures: [],
|
|
4111
|
-
storage: [],
|
|
4112
|
-
cookies: [],
|
|
4113
|
-
tags: [],
|
|
4114
|
-
});
|
|
4115
|
-
const keys = await store.storedkeys();
|
|
4116
|
-
expect(keys).toContain("runs");
|
|
4117
|
-
expect(keys).toContain("runlogrun-one");
|
|
4118
|
-
expect(keys).toContain("runscopesrun-one");
|
|
4119
|
-
expect(keys).toContain("runtimeline:run-one");
|
|
4120
|
-
expect(keys).toContain("provlog:run-one");
|
|
4121
|
-
expect(keys).toContain("runlogrun-two");
|
|
4122
|
-
expect(keys).toContain("trailsession");
|
|
4123
|
-
expect(keys).toContain("audit");
|
|
4124
|
-
});
|
|
4125
|
-
|
|
4126
|
-
it("purges every trace key of one run while the other runs and the audit family survive", async () => {
|
|
4127
|
-
const store = new sessionmemory(new fakeadapter());
|
|
4128
|
-
await store.setrunrecord({
|
|
4129
|
-
runid: "run-one",
|
|
4130
|
-
planid: "plan",
|
|
4131
|
-
sessionid: "session",
|
|
4132
|
-
state: "completed",
|
|
4133
|
-
createdat: 1,
|
|
4134
|
-
updatedat: 2,
|
|
4135
|
-
});
|
|
4136
|
-
await store.setrunrecord({
|
|
4137
|
-
runid: "run-two",
|
|
4138
|
-
planid: "plan",
|
|
4139
|
-
sessionid: "session",
|
|
4140
|
-
state: "running",
|
|
4141
|
-
createdat: 3,
|
|
4142
|
-
updatedat: 4,
|
|
4143
|
-
});
|
|
4144
|
-
await store.setrunscopes("run-one", []);
|
|
4145
|
-
await store.setrunscopes("run-two", []);
|
|
4146
|
-
const keys = await store.storedkeys();
|
|
4147
|
-
const trace = keys.filter((key) => key.includes("run-one"));
|
|
4148
|
-
expect(trace.length).toBeGreaterThan(0);
|
|
4149
|
-
await store.purgekeys(trace);
|
|
4150
|
-
expect(await store.getrunscopes("run-one")).toEqual([]);
|
|
4151
|
-
expect(keys.filter((key) => key.includes("run-two")).length).toBeGreaterThan(0);
|
|
4152
|
-
expect(keys).toContain("runs");
|
|
4153
|
-
expect(keys).toContain("audit");
|
|
4154
|
-
});
|
|
4155
|
-
});
|