@reticlehq/server 3.9.0 → 3.10.0
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/SKILL.md +13 -12
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -187
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +46 -41
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-runtime.js +6 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +44 -0
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +28 -9
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +1 -1
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +2 -2
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +13 -13
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge cases, driven: the inputs and the interaction orders the plain explorer never tries, run on
|
|
3
|
+
* the states it found, and judged by the checks in `invariants.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The order is the order defects are found in. Controls whose click sent a write come first: a
|
|
6
|
+
* submit pressed twice and a double click on them, then a reload after their write. Edge-case
|
|
7
|
+
* values for each field come next, one round across every field before the next value of any field.
|
|
8
|
+
* Controls that only read come after, then the rest of the sequences, then cancel controls.
|
|
9
|
+
*
|
|
10
|
+
* Each probe replays its state from a fresh start page, so the budget buys whole probes in that
|
|
11
|
+
* order and the caller keeps it apart from exploration's. Deterministic: the same states, port and
|
|
12
|
+
* clock give the same probes in the same order.
|
|
13
|
+
*/
|
|
14
|
+
import { EdgeAction, EdgeInteraction, describeStep, edgeInputs, edgeInteractions, looksLikeSubmit, } from '../edge-cases.js';
|
|
15
|
+
import { controlsOf, } from '../explorer.js';
|
|
16
|
+
import { CrawlAnomalyKind } from '@reticlehq/core';
|
|
17
|
+
import { isActivity } from '../session-fold.js';
|
|
18
|
+
import { Invariant, samePage, checkDeterministic, checkReloadKeeps, checkRepeat, checkUndo, } from '../invariants.js';
|
|
19
|
+
import { faultsOf, runEdge, shownVsSent } from './edge-run.js';
|
|
20
|
+
/** Edge-case values per field. `edgeInputs` puts the most telling first. */
|
|
21
|
+
export const VALUES_PER_FIELD = 6;
|
|
22
|
+
/** A value shorter than this is found on almost any page, so its absence after a reload says nothing. */
|
|
23
|
+
const MIN_KEPT_VALUE = 3;
|
|
24
|
+
const CANCEL = /^button\s+"(?:cancel|close|undo|dismiss)\b/i;
|
|
25
|
+
const FIELD_LINE = /^\s*-?\s*(?:textbox|searchbox|spinbutton|combobox)\b/;
|
|
26
|
+
const SETTLE = { action: EdgeAction.SETTLE };
|
|
27
|
+
const OK_MIN = 200;
|
|
28
|
+
const OK_END = 300;
|
|
29
|
+
const click = (key) => ({ action: EdgeAction.CLICK, key });
|
|
30
|
+
/** Each control that sends a request or looks like a submit, once, on the first state it is on. */
|
|
31
|
+
function targetsOf(surfaces) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const surface of surfaces) {
|
|
35
|
+
for (const control of surface.order) {
|
|
36
|
+
if (seen.has(control) || (!surface.requests.includes(control) && !looksLikeSubmit(control))) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
seen.add(control);
|
|
40
|
+
out.push({ surface, control, writes: surface.writes.includes(control) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** A field's spec from its control key and the attributes the page gave. */
|
|
46
|
+
export function specOf(control, attrs = {}) {
|
|
47
|
+
const [, role = '', name] = /^(\S+)(?:\s+"([^"]*)")?/.exec(control) ?? [];
|
|
48
|
+
const count = (raw) => {
|
|
49
|
+
const n = Number(raw);
|
|
50
|
+
return raw === undefined || !Number.isInteger(n) ? undefined : n;
|
|
51
|
+
};
|
|
52
|
+
const optional = {
|
|
53
|
+
name,
|
|
54
|
+
type: attrs['type'],
|
|
55
|
+
inputMode: attrs['inputmode'],
|
|
56
|
+
min: attrs['min'],
|
|
57
|
+
max: attrs['max'],
|
|
58
|
+
pattern: attrs['pattern'],
|
|
59
|
+
maxLength: count(attrs['maxlength']),
|
|
60
|
+
minLength: count(attrs['minlength']),
|
|
61
|
+
};
|
|
62
|
+
const spec = { role, required: attrs['required'] !== undefined };
|
|
63
|
+
for (const [key, value] of Object.entries(optional)) {
|
|
64
|
+
if (value !== undefined) {
|
|
65
|
+
Object.assign(spec, { [key]: value });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return spec;
|
|
69
|
+
}
|
|
70
|
+
/** The writes in `twice` beyond those `once` sent: a second request to the same endpoint. */
|
|
71
|
+
function extraWrites(twice, once) {
|
|
72
|
+
const left = [...once];
|
|
73
|
+
return twice.filter((w) => {
|
|
74
|
+
const index = left.findIndex((o) => o.key === w.key);
|
|
75
|
+
if (index < 0) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
left.splice(index, 1);
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const shownOutsideFields = (page, value) => page.tree.split('\n').some((line) => !FIELD_LINE.test(line) && line.includes(value));
|
|
83
|
+
/** Two pages are one view: the same route and the same controls, whatever the fields hold. */
|
|
84
|
+
const sameView = (a, b) => {
|
|
85
|
+
const keys = (page) => [...new Set(controlsOf(page.tree).map((c) => c.key))].sort().join('\n');
|
|
86
|
+
return a.route === b.route && keys(a) === keys(b);
|
|
87
|
+
};
|
|
88
|
+
const sourceOf = (run) => run.source === undefined ? {} : { source: run.source };
|
|
89
|
+
export async function driveEdges(port, surfaces, opts) {
|
|
90
|
+
let actions = 0;
|
|
91
|
+
let failed = 0;
|
|
92
|
+
const spent = () => actions >= opts.maxActions;
|
|
93
|
+
const known = new Set(opts.known ?? []);
|
|
94
|
+
const found = [];
|
|
95
|
+
const probed = new Set();
|
|
96
|
+
const driver = {
|
|
97
|
+
port,
|
|
98
|
+
act: (ref, action, value, actOpts) => {
|
|
99
|
+
actions += 1;
|
|
100
|
+
return port.act(ref, action, value, actOpts);
|
|
101
|
+
},
|
|
102
|
+
move: (action) => {
|
|
103
|
+
const move = EdgeAction.BACK === action ? port.back : port.reload;
|
|
104
|
+
if (move === undefined) {
|
|
105
|
+
return Promise.resolve(false);
|
|
106
|
+
}
|
|
107
|
+
actions += 1;
|
|
108
|
+
return move();
|
|
109
|
+
},
|
|
110
|
+
spent,
|
|
111
|
+
...(opts.wait === undefined ? {} : { wait: opts.wait }),
|
|
112
|
+
};
|
|
113
|
+
const report = (control, findings) => {
|
|
114
|
+
for (const finding of findings) {
|
|
115
|
+
found.push({ control, finding });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/** Run, report the faults any run can show, and hand the run to `judge` for the rest. */
|
|
119
|
+
const probe = async (target, steps, shown, judge = () => []) => {
|
|
120
|
+
const run = await runEdge(driver, target.surface.path, steps);
|
|
121
|
+
if (run !== undefined) {
|
|
122
|
+
report(target.control, [
|
|
123
|
+
...faultsOf(run, target.control, shown, known),
|
|
124
|
+
...shownVsSent(run, shown),
|
|
125
|
+
...judge(run),
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
return run;
|
|
129
|
+
};
|
|
130
|
+
const baselines = new Map();
|
|
131
|
+
/** The control pressed once after its fills, with a settle before the press. */
|
|
132
|
+
const baseline = async (t) => {
|
|
133
|
+
if (!baselines.has(t.control)) {
|
|
134
|
+
const steps = [...t.surface.fills, SETTLE, click(t.control)];
|
|
135
|
+
baselines.set(t.control, await probe(t, steps, [...t.surface.fills, click(t.control)]));
|
|
136
|
+
}
|
|
137
|
+
return baselines.get(t.control);
|
|
138
|
+
};
|
|
139
|
+
const sequence = (t, kind) => {
|
|
140
|
+
const seq = edgeInteractions({
|
|
141
|
+
control: t.control,
|
|
142
|
+
order: t.surface.order,
|
|
143
|
+
fills: t.surface.fills,
|
|
144
|
+
}).find((s) => s.kind === kind);
|
|
145
|
+
if (seq === undefined) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
// The two submit sequences place the fills themselves; every other one starts from a filled form.
|
|
149
|
+
const own = EdgeInteraction.SUBMIT_TWICE === kind || EdgeInteraction.SUBMIT_BEFORE_FILL === kind;
|
|
150
|
+
return own ? seq.steps : [...t.surface.fills, ...seq.steps];
|
|
151
|
+
};
|
|
152
|
+
const submitTwice = async (t) => {
|
|
153
|
+
const shown = sequence(t, EdgeInteraction.SUBMIT_TWICE);
|
|
154
|
+
if (shown === undefined) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const fills = t.surface.fills.length;
|
|
158
|
+
const steps = [...shown.slice(0, fills), SETTLE, ...shown.slice(fills)];
|
|
159
|
+
const run = await probe(t, steps, shown, (r) => checkRepeat({
|
|
160
|
+
before: r.marks[0] ?? r.before,
|
|
161
|
+
afterFirst: r.marks[1] ?? r.after,
|
|
162
|
+
afterSecond: r.after,
|
|
163
|
+
firstWrites: r.writes[1] ?? [],
|
|
164
|
+
secondWrites: r.writes[2] ?? [],
|
|
165
|
+
steps: shown,
|
|
166
|
+
...sourceOf(r),
|
|
167
|
+
}));
|
|
168
|
+
return run !== undefined;
|
|
169
|
+
};
|
|
170
|
+
/** Pressed back to back, against a run pressed once. Only writes the single press did not send count. */
|
|
171
|
+
const repeated = async (t, kind) => {
|
|
172
|
+
const shown = sequence(t, kind);
|
|
173
|
+
const once = await baseline(t);
|
|
174
|
+
if (shown === undefined || once === undefined) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const fills = t.surface.fills.length;
|
|
178
|
+
const steps = [...shown.slice(0, fills), SETTLE, ...shown.slice(fills)];
|
|
179
|
+
const run = await probe(t, steps, shown, (r) => checkRepeat({
|
|
180
|
+
before: once.marks[0] ?? once.before,
|
|
181
|
+
afterFirst: once.after,
|
|
182
|
+
afterSecond: r.after,
|
|
183
|
+
firstWrites: once.writes[1] ?? [],
|
|
184
|
+
secondWrites: extraWrites(r.writes[1] ?? [], once.writes[1] ?? []),
|
|
185
|
+
steps: shown,
|
|
186
|
+
...sourceOf(r),
|
|
187
|
+
}));
|
|
188
|
+
return run !== undefined;
|
|
189
|
+
};
|
|
190
|
+
/** A write, a settle, then a reload: every value the saved page showed must still be there. */
|
|
191
|
+
const reloadKeeps = async (t) => {
|
|
192
|
+
const shown = [...t.surface.fills, click(t.control)];
|
|
193
|
+
const steps = [...shown, SETTLE, { action: EdgeAction.RELOAD }];
|
|
194
|
+
const run = await probe(t, steps, shown, (r) => {
|
|
195
|
+
const saved = r.marks[0];
|
|
196
|
+
const ok = (r.writes[0] ?? []).some((w) => w.status !== undefined && w.status >= OK_MIN && w.status < OK_END);
|
|
197
|
+
// The reload must land on the view that showed the save. An app without deep links lands
|
|
198
|
+
// elsewhere, which loses the user's place, not the value, and the value cannot be looked for.
|
|
199
|
+
if (saved === undefined || !ok || !sameView(saved, r.after)) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
return t.surface.fills.flatMap((fill) => {
|
|
203
|
+
const value = (fill.value ?? '').trim();
|
|
204
|
+
if (value.length < MIN_KEPT_VALUE || !shownOutsideFields(saved, value)) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
return checkReloadKeeps({ value, afterReload: r.after, steps: shown, ...sourceOf(r) });
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
return run !== undefined;
|
|
211
|
+
};
|
|
212
|
+
const deterministic = async (t) => {
|
|
213
|
+
const once = await baseline(t);
|
|
214
|
+
if (once === undefined) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
const shown = [...t.surface.fills, click(t.control)];
|
|
218
|
+
const run = await probe(t, [...t.surface.fills, SETTLE, click(t.control)], shown, (r) => checkDeterministic({ first: once.after, second: r.after, steps: shown, ...sourceOf(r) }));
|
|
219
|
+
return run !== undefined;
|
|
220
|
+
};
|
|
221
|
+
const plain = async (t, kind) => {
|
|
222
|
+
const steps = sequence(t, kind);
|
|
223
|
+
return steps !== undefined && (await probe(t, steps, steps)) !== undefined;
|
|
224
|
+
};
|
|
225
|
+
/** One edge value in one field, the other fields as the explorer filled them, then the submit. */
|
|
226
|
+
const edgeInput = async (surface, submit, field, value) => {
|
|
227
|
+
const fill = { key: field, action: EdgeAction.FILL, value };
|
|
228
|
+
// A pre-filled field is not among the explorer's fills: it gets its fill after them.
|
|
229
|
+
const fills = surface.fills.some((f) => f.key === field)
|
|
230
|
+
? surface.fills.map((f) => (f.key === field ? fill : f))
|
|
231
|
+
: [...surface.fills, fill];
|
|
232
|
+
const steps = [...fills, click(submit)];
|
|
233
|
+
return (await probe({ surface, control: field }, steps, steps)) !== undefined;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* A control the explorer saw do nothing, pressed again. Dead only when this press shows no
|
|
237
|
+
* activity either and the whole page, text included, is the same before and after it.
|
|
238
|
+
*/
|
|
239
|
+
const dead = async (surface, control) => {
|
|
240
|
+
const steps = [...surface.fills, click(control)];
|
|
241
|
+
const run = await runEdge(driver, surface.path, steps);
|
|
242
|
+
if (run === undefined) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (!run.events.some(isActivity) && samePage(run.before, run.after)) {
|
|
246
|
+
report(control, [
|
|
247
|
+
{
|
|
248
|
+
kind: CrawlAnomalyKind.DEAD_CONTROL,
|
|
249
|
+
invariant: Invariant.RESPONDS,
|
|
250
|
+
detail: 'pressed twice, from a fresh page each time, and the app did nothing: no request, no page change, no route change',
|
|
251
|
+
sequence: steps.map(describeStep),
|
|
252
|
+
...(run.source === undefined
|
|
253
|
+
? {}
|
|
254
|
+
: { source: `${run.source.file}:${String(run.source.line)}` }),
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
};
|
|
260
|
+
/** A cancel on a dialog: the page after it must be the page before the dialog opened. */
|
|
261
|
+
const undo = async (surface, cancel) => {
|
|
262
|
+
const open = surface.path.at(-1);
|
|
263
|
+
if (open === undefined || EdgeAction.CLICK !== open.action) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
const shown = [click(open.key), click(cancel)];
|
|
267
|
+
const run = await runEdge(driver, surface.path.slice(0, -1), [
|
|
268
|
+
click(open.key),
|
|
269
|
+
SETTLE,
|
|
270
|
+
click(cancel),
|
|
271
|
+
]);
|
|
272
|
+
if (run === undefined) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
// Only a dialog: a cancel that leaves for another page loads it fresh, and a fresh page is
|
|
276
|
+
// not the page the user left.
|
|
277
|
+
if (run.marks[0]?.route === run.before.route) {
|
|
278
|
+
report(cancel, checkUndo({ before: run.before, after: run.after, steps: shown, ...sourceOf(run) }));
|
|
279
|
+
}
|
|
280
|
+
report(cancel, faultsOf(run, cancel, shown, known));
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
283
|
+
const targets = targetsOf(surfaces);
|
|
284
|
+
const writers = targets.filter((t) => t.writes);
|
|
285
|
+
const readers = targets.filter((t) => !t.writes);
|
|
286
|
+
const probes = [];
|
|
287
|
+
const add = (control, run) => {
|
|
288
|
+
probes.push({ control, run });
|
|
289
|
+
};
|
|
290
|
+
// Every control that writes gets one probe before any gets a second: a double press first, which
|
|
291
|
+
// also sends the write once to compare with.
|
|
292
|
+
for (const t of writers) {
|
|
293
|
+
add(t.control, () => repeated(t, EdgeInteraction.DOUBLE_ACTIVATION));
|
|
294
|
+
}
|
|
295
|
+
for (const surface of surfaces) {
|
|
296
|
+
for (const control of surface.inert) {
|
|
297
|
+
add(control, () => dead(surface, control));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const t of writers) {
|
|
301
|
+
add(t.control, () => submitTwice(t));
|
|
302
|
+
}
|
|
303
|
+
for (const t of writers) {
|
|
304
|
+
add(t.control, () => reloadKeeps(t));
|
|
305
|
+
}
|
|
306
|
+
const inputs = surfaces.flatMap((surface) => {
|
|
307
|
+
const submit = surface.writes[0] ?? surface.requests[0] ?? surface.order.find((c) => looksLikeSubmit(c));
|
|
308
|
+
if (submit === undefined) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
return surface.fields.map((field) => ({
|
|
312
|
+
surface,
|
|
313
|
+
submit,
|
|
314
|
+
field,
|
|
315
|
+
values: edgeInputs(specOf(field, surface.attrs[field]), {
|
|
316
|
+
now: opts.now,
|
|
317
|
+
max: VALUES_PER_FIELD,
|
|
318
|
+
}),
|
|
319
|
+
}));
|
|
320
|
+
});
|
|
321
|
+
for (let round = 0; round < VALUES_PER_FIELD; round += 1) {
|
|
322
|
+
for (const i of inputs) {
|
|
323
|
+
const edge = i.values[round];
|
|
324
|
+
if (edge !== undefined) {
|
|
325
|
+
add(i.field, () => edgeInput(i.surface, i.submit, i.field, edge.value));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const t of readers) {
|
|
330
|
+
add(t.control, () => repeated(t, EdgeInteraction.DOUBLE_ACTIVATION));
|
|
331
|
+
add(t.control, () => deterministic(t));
|
|
332
|
+
}
|
|
333
|
+
for (const t of targets) {
|
|
334
|
+
add(t.control, () => repeated(t, EdgeInteraction.RAPID_REPEAT));
|
|
335
|
+
add(t.control, () => plain(t, EdgeInteraction.ACT_THEN_BACK));
|
|
336
|
+
add(t.control, () => plain(t, EdgeInteraction.SUBMIT_BEFORE_FILL));
|
|
337
|
+
add(t.control, () => plain(t, EdgeInteraction.OUT_OF_ORDER));
|
|
338
|
+
}
|
|
339
|
+
for (const surface of surfaces) {
|
|
340
|
+
for (const cancel of surface.order.filter((c) => CANCEL.test(c))) {
|
|
341
|
+
add(cancel, () => undo(surface, cancel));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const p of probes) {
|
|
345
|
+
if (spent()) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
if (await p.run()) {
|
|
350
|
+
probed.add(p.control);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
failed += 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
probed: [...probed],
|
|
359
|
+
found,
|
|
360
|
+
actions,
|
|
361
|
+
budgetExhausted: spent(),
|
|
362
|
+
failed,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One edge sequence, driven, and what its run shows without a reference answer.
|
|
3
|
+
*
|
|
4
|
+
* `runEdge` replays a state from a fresh start page, runs the steps, and reads what the checks in
|
|
5
|
+
* `invariants.ts` compare: the page before the steps, the page at each settle, the page at the end,
|
|
6
|
+
* and the writes of each stretch between settles. Two steps with no settle between them run back
|
|
7
|
+
* to back, which is how a double click reaches the app before the first click's request comes back.
|
|
8
|
+
*
|
|
9
|
+
* `faultsOf` and `shownVsSent` judge any run. A fault the plain click of the same control already
|
|
10
|
+
* showed is the explorer's finding, not an edge case's, so the caller passes it in `known`.
|
|
11
|
+
*/
|
|
12
|
+
import { type ReticleEvent } from '@reticlehq/core';
|
|
13
|
+
import { EdgeAction, type EdgeStep } from '../edge-cases.js';
|
|
14
|
+
import { type ActResult, type ExplorePort, type Step } from '../explorer.js';
|
|
15
|
+
import { type PageObservation, type WriteObservation } from '../invariants.js';
|
|
16
|
+
import type { BehaviourFinding } from '../ledger.js';
|
|
17
|
+
export interface EdgeRun {
|
|
18
|
+
/** The page after the state was replayed, before the first step. */
|
|
19
|
+
before: PageObservation;
|
|
20
|
+
/** The page at each settle step, in order. */
|
|
21
|
+
marks: PageObservation[];
|
|
22
|
+
/** The page after the last step. */
|
|
23
|
+
after: PageObservation;
|
|
24
|
+
/** The writes of each stretch between settle steps. */
|
|
25
|
+
writes: WriteObservation[][];
|
|
26
|
+
/** Every event the steps caused. */
|
|
27
|
+
events: ReticleEvent[];
|
|
28
|
+
/** The source of the first control the run acted on, when the app gives it. */
|
|
29
|
+
source?: {
|
|
30
|
+
file: string;
|
|
31
|
+
line: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The port, with the budget the caller counts. */
|
|
35
|
+
export interface EdgeDriver {
|
|
36
|
+
port: ExplorePort;
|
|
37
|
+
act: (ref: string, action: 'click' | 'fill', value?: string, opts?: {
|
|
38
|
+
settle?: boolean;
|
|
39
|
+
}) => Promise<ActResult>;
|
|
40
|
+
/** Back or reload, counted as one action. */
|
|
41
|
+
move: (action: typeof EdgeAction.BACK | typeof EdgeAction.RELOAD) => Promise<boolean>;
|
|
42
|
+
spent: () => boolean;
|
|
43
|
+
wait?: () => Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/** Replay `path`, then run `steps`. Undefined when the state or a step's control was not there. */
|
|
46
|
+
export declare function runEdge(d: EdgeDriver, path: readonly Step[], steps: readonly EdgeStep[]): Promise<EdgeRun | undefined>;
|
|
47
|
+
/**
|
|
48
|
+
* The faults the run's events show: page errors, server errors and contradictions. A fault already
|
|
49
|
+
* in `known` (as `control\nkind`) is skipped, and each new one is added, so it is reported once.
|
|
50
|
+
*/
|
|
51
|
+
export declare function faultsOf(run: EdgeRun, control: string, steps: readonly EdgeStep[], known: Set<string>): BehaviourFinding[];
|
|
52
|
+
/** The value a field shows on a page, when the field is there and shows one. */
|
|
53
|
+
export declare function shownValue(page: PageObservation, field: string): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* For each field the steps filled: when a write sent a value under the field's name, the field
|
|
56
|
+
* must show that value after the write. Matched by name only.
|
|
57
|
+
* ponytail: top-level JSON keys only; a nested or form-encoded body is not read.
|
|
58
|
+
*/
|
|
59
|
+
export declare function shownVsSent(run: EdgeRun, steps: readonly EdgeStep[]): BehaviourFinding[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One edge sequence, driven, and what its run shows without a reference answer.
|
|
3
|
+
*
|
|
4
|
+
* `runEdge` replays a state from a fresh start page, runs the steps, and reads what the checks in
|
|
5
|
+
* `invariants.ts` compare: the page before the steps, the page at each settle, the page at the end,
|
|
6
|
+
* and the writes of each stretch between settles. Two steps with no settle between them run back
|
|
7
|
+
* to back, which is how a double click reaches the app before the first click's request comes back.
|
|
8
|
+
*
|
|
9
|
+
* `faultsOf` and `shownVsSent` judge any run. A fault the plain click of the same control already
|
|
10
|
+
* showed is the explorer's finding, not an edge case's, so the caller passes it in `known`.
|
|
11
|
+
*/
|
|
12
|
+
import { REDACTED_VALUE, asNumber, asString } from '@reticlehq/core';
|
|
13
|
+
import { EdgeAction, describeStep } from '../edge-cases.js';
|
|
14
|
+
import { controlsOf, noteAnomalies, replayPath, } from '../explorer.js';
|
|
15
|
+
import { Invariant, checkShownMatchesSent, } from '../invariants.js';
|
|
16
|
+
import { controlKey, writeKeyOf } from '../session-fold.js';
|
|
17
|
+
/** A server error is a fault whatever the input was. A 4xx is the server refusing bad input. */
|
|
18
|
+
const SERVER_FAULT = 500;
|
|
19
|
+
const VALUE_ATTR = /\[value="((?:[^"\\]|\\.)*)"/;
|
|
20
|
+
/** Letters and digits only, lower case: `First name` and `firstName` are one name. */
|
|
21
|
+
const bare = (name) => name.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
|
22
|
+
const writesOf = (events) => events.flatMap((e) => {
|
|
23
|
+
const key = writeKeyOf(e);
|
|
24
|
+
if (key === undefined) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const status = asNumber(e.data['status']);
|
|
28
|
+
const response = asString(e.data['responseBody']);
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
key,
|
|
32
|
+
...(status === undefined ? {} : { status }),
|
|
33
|
+
...(response === undefined ? {} : { response }),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
});
|
|
37
|
+
/** Replay `path`, then run `steps`. Undefined when the state or a step's control was not there. */
|
|
38
|
+
export async function runEdge(d, path, steps) {
|
|
39
|
+
if (!(await replayPath(d.port, path, d.act, d.spent))) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const read = () => (d.port.lookFull ?? d.port.look)();
|
|
43
|
+
const before = await read();
|
|
44
|
+
let controls = controlsOf((await d.port.look()).tree);
|
|
45
|
+
const marks = [];
|
|
46
|
+
const stretches = [[]];
|
|
47
|
+
let source;
|
|
48
|
+
for (const [index, step] of steps.entries()) {
|
|
49
|
+
if (d.spent()) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if ('key' in step) {
|
|
53
|
+
const control = controls.find((c) => c.key === step.key);
|
|
54
|
+
if (control === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const next = steps[index + 1];
|
|
58
|
+
const settle = next === undefined || EdgeAction.SETTLE === next.action;
|
|
59
|
+
const result = await d.act(control.ref, step.action, step.value, { settle });
|
|
60
|
+
source ??= result.source;
|
|
61
|
+
stretches.at(-1)?.push(...result.events);
|
|
62
|
+
}
|
|
63
|
+
else if (EdgeAction.SETTLE === step.action) {
|
|
64
|
+
await d.wait?.();
|
|
65
|
+
marks.push(await read());
|
|
66
|
+
controls = controlsOf((await d.port.look()).tree);
|
|
67
|
+
stretches.push([]);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (!(await d.move(step.action))) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
controls = controlsOf((await d.port.look()).tree);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await d.wait?.();
|
|
77
|
+
const after = await read();
|
|
78
|
+
return {
|
|
79
|
+
before,
|
|
80
|
+
marks,
|
|
81
|
+
after,
|
|
82
|
+
writes: stretches.map(writesOf),
|
|
83
|
+
events: stretches.flat(),
|
|
84
|
+
...(source === undefined ? {} : { source }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const findingFrom = (kind, detail, steps, source) => ({
|
|
88
|
+
kind,
|
|
89
|
+
invariant: Invariant.NO_FAULT,
|
|
90
|
+
detail,
|
|
91
|
+
sequence: steps.map(describeStep),
|
|
92
|
+
...(source === undefined ? {} : { source: `${source.file}:${String(source.line)}` }),
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* The faults the run's events show: page errors, server errors and contradictions. A fault already
|
|
96
|
+
* in `known` (as `control\nkind`) is skipped, and each new one is added, so it is reported once.
|
|
97
|
+
*/
|
|
98
|
+
export function faultsOf(run, control, steps, known) {
|
|
99
|
+
const anomalies = [];
|
|
100
|
+
noteAnomalies(run.events, control, anomalies, SERVER_FAULT);
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const a of anomalies) {
|
|
103
|
+
const id = `${control}\n${a.kind}`;
|
|
104
|
+
if (!known.has(id)) {
|
|
105
|
+
known.add(id);
|
|
106
|
+
out.push(findingFrom(a.kind, a.detail, steps, run.source));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/** The value a field shows on a page, when the field is there and shows one. */
|
|
112
|
+
export function shownValue(page, field) {
|
|
113
|
+
for (const line of page.tree.split('\n')) {
|
|
114
|
+
if (controlKey(line) === field) {
|
|
115
|
+
return VALUE_ATTR.exec(line)?.[1];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
/** The top-level values of a JSON object body, by bare name. Empty for any other body. */
|
|
121
|
+
function bodyValues(event) {
|
|
122
|
+
const out = new Map();
|
|
123
|
+
const text = asString(event.data['requestBody']);
|
|
124
|
+
if (text === undefined) {
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const body = JSON.parse(text);
|
|
129
|
+
if ('object' !== typeof body || null === body || Array.isArray(body)) {
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
for (const [key, value] of Object.entries(body)) {
|
|
133
|
+
if ('string' === typeof value || 'number' === typeof value) {
|
|
134
|
+
out.set(bare(key), String(value));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* For each field the steps filled: when a write sent a value under the field's name, the field
|
|
145
|
+
* must show that value after the write. Matched by name only.
|
|
146
|
+
* ponytail: top-level JSON keys only; a nested or form-encoded body is not read.
|
|
147
|
+
*/
|
|
148
|
+
export function shownVsSent(run, steps) {
|
|
149
|
+
const sent = run.events.filter((e) => writeKeyOf(e) !== undefined).map(bodyValues);
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const step of steps) {
|
|
152
|
+
if (!('key' in step) || EdgeAction.FILL !== step.action) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const name = bare(/"([^"]*)"/.exec(step.key)?.[1] ?? '');
|
|
156
|
+
const value = sent.map((body) => body.get(name)).find((v) => v !== undefined);
|
|
157
|
+
if (0 === name.length || value === undefined || REDACTED_VALUE === value) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
out.push(...checkShownMatchesSent({
|
|
161
|
+
field: step.key,
|
|
162
|
+
sent: value,
|
|
163
|
+
shown: shownValue(run.after, step.key),
|
|
164
|
+
steps,
|
|
165
|
+
...(run.source === undefined ? {} : { source: run.source }),
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|