@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
|
@@ -13,27 +13,55 @@
|
|
|
13
13
|
* - Controls that differ only by a number — the rows of a table — are one control. It is covered
|
|
14
14
|
* once, which is what keeps a 500-row page from eating the whole budget.
|
|
15
15
|
* - Text fields on a state are filled before its buttons are tried, so a submit is driven with
|
|
16
|
-
* input rather than dismissed as dead because it refused an empty form.
|
|
16
|
+
* input rather than dismissed as dead because it refused an empty form. Each field gets its own
|
|
17
|
+
* value (`fillValue` is told the field's position), so two fields a build swaps do not read alike.
|
|
18
|
+
* - `paths` keeps every click it tried, also a click that kept the same state (a count went up).
|
|
19
|
+
* A caller that reads the page after each path needs those clicks too.
|
|
17
20
|
*
|
|
18
21
|
* ponytail: breadth-first with a flat action budget, and every edge replays its path from a fresh
|
|
19
22
|
* load. That is O(states × path length), correct and slow; add prefix sharing if it proves too slow.
|
|
20
23
|
* A control that needs a choice (select, combobox) is not driven — it needs a value model.
|
|
21
24
|
*/
|
|
22
25
|
import { type ReticleEvent } from '@reticlehq/core';
|
|
26
|
+
/** What one action gives back. */
|
|
27
|
+
export interface ActResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
/** The events this action caused. */
|
|
30
|
+
events: readonly ReticleEvent[];
|
|
31
|
+
/** The element's source, when the app gives source attribution. */
|
|
32
|
+
source?: {
|
|
33
|
+
file: string;
|
|
34
|
+
line: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** A page as the explorer reads it: the snapshot tree and the route. */
|
|
38
|
+
export interface PageRead {
|
|
39
|
+
tree: string;
|
|
40
|
+
route?: string;
|
|
41
|
+
}
|
|
42
|
+
/** The attributes `fieldAttrs` reads: what a field takes, for its edge-case values. */
|
|
43
|
+
export declare const FIELD_ATTRS: readonly ["type", "min", "max", "maxlength", "minlength", "pattern", "required", "inputmode"];
|
|
23
44
|
/** What the explorer needs from a driven page. The tool layer adapts a session to it. */
|
|
24
45
|
export interface ExplorePort {
|
|
25
46
|
/** Load the start page fresh. False when it did not come back. */
|
|
26
47
|
reset(): Promise<boolean>;
|
|
27
48
|
/** The interactive tree and route of the page now. */
|
|
28
|
-
look(): Promise<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
act(ref: string, action: 'click' | 'fill', value?: string
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
look(): Promise<PageRead>;
|
|
50
|
+
/**
|
|
51
|
+
* Act on a control. `settle: false` returns as soon as the action is sent, so the next action
|
|
52
|
+
* runs back to back with it; its late events then arrive with the next action's.
|
|
53
|
+
*/
|
|
54
|
+
act(ref: string, action: 'click' | 'fill', value?: string, opts?: {
|
|
55
|
+
settle?: boolean;
|
|
56
|
+
}): Promise<ActResult>;
|
|
57
|
+
/** The whole page, text included. Absent when the port reads only the interactive tree. */
|
|
58
|
+
lookFull?: () => Promise<PageRead>;
|
|
59
|
+
/** Go back to the page before the last action. False when it did not come back. */
|
|
60
|
+
back?: () => Promise<boolean>;
|
|
61
|
+
/** Reload the page now. False when it did not come back. */
|
|
62
|
+
reload?: () => Promise<boolean>;
|
|
63
|
+
/** The `FIELD_ATTRS` the field `control` (a control key) carries, for its edge-case values. */
|
|
64
|
+
fieldAttrs?: (control: string) => Promise<Record<string, string> | undefined>;
|
|
37
65
|
/** Install network mocks ([] clears). Absent, or false, when no driven browser can. */
|
|
38
66
|
mock?: (rules: {
|
|
39
67
|
urlContains: string;
|
|
@@ -57,6 +85,33 @@ export interface ExploredWrite {
|
|
|
57
85
|
beforeState: string;
|
|
58
86
|
/** The state the app reached when this write SUCCEEDED — what a failure must not look like. */
|
|
59
87
|
successState: string;
|
|
88
|
+
/**
|
|
89
|
+
* True when the write answered an error or never completed while exploring. `successState` is
|
|
90
|
+
* then where a failure lands, not a success, and nothing can be judged by landing there.
|
|
91
|
+
*/
|
|
92
|
+
failed?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** One explored state, as the edge-case drive needs it. */
|
|
95
|
+
export interface ExploredSurface {
|
|
96
|
+
/** How to reach the state from a fresh start page, before its fields are filled. */
|
|
97
|
+
path: Step[];
|
|
98
|
+
/** What the explorer typed into the state's empty fields, in page order. */
|
|
99
|
+
fills: Step[];
|
|
100
|
+
/** Every text field on the state, in page order: the empty ones and the pre-filled ones. */
|
|
101
|
+
fields: string[];
|
|
102
|
+
/** Each field's `FIELD_ATTRS`, by control key. */
|
|
103
|
+
attrs: Record<string, Record<string, string>>;
|
|
104
|
+
/** The controls the explorer clicked on this state, in page order. */
|
|
105
|
+
order: string[];
|
|
106
|
+
/** The controls whose click sent a request. */
|
|
107
|
+
requests: string[];
|
|
108
|
+
/** The controls whose click sent a write. */
|
|
109
|
+
writes: string[];
|
|
110
|
+
/**
|
|
111
|
+
* The controls whose click showed no activity at all: no request, no DOM change, no route change,
|
|
112
|
+
* no signal. One sample; the edge drive presses each again before it calls one dead.
|
|
113
|
+
*/
|
|
114
|
+
inert: string[];
|
|
60
115
|
}
|
|
61
116
|
export interface ExploreReport {
|
|
62
117
|
states: number;
|
|
@@ -80,19 +135,29 @@ export interface ExploreReport {
|
|
|
80
135
|
/** States whose recorded path no longer replays — the app is not deterministic from its start. */
|
|
81
136
|
unreachable: number;
|
|
82
137
|
budgetExhausted: boolean;
|
|
138
|
+
/** Every click tried, as its path from a fresh start page, in the order the explorer tried them. */
|
|
139
|
+
paths: Step[][];
|
|
140
|
+
/** Every explored state, with its fields, controls and the controls that sent a request. */
|
|
141
|
+
surfaces: ExploredSurface[];
|
|
142
|
+
}
|
|
143
|
+
export interface ExploreOptions {
|
|
144
|
+
maxActions: number;
|
|
145
|
+
/** What to type into an empty field: its label, and its position among the fields on the state. */
|
|
146
|
+
fillValue: (label: string, index: number) => string;
|
|
147
|
+
/** Called after each click, before the page is read, so behaviour driven by a timer can show. */
|
|
148
|
+
wait?: () => Promise<void>;
|
|
83
149
|
}
|
|
84
150
|
interface Control {
|
|
85
151
|
ref: string;
|
|
86
152
|
key: string;
|
|
87
153
|
/** The field already holds a value — pre-filled credentials, a default. Typing over it breaks it. */
|
|
88
154
|
holdsValue: boolean;
|
|
155
|
+
/** The snapshot line, with the states (`[disabled]`) the key leaves out. */
|
|
156
|
+
desc: string;
|
|
89
157
|
}
|
|
90
158
|
export declare function controlsOf(tree: string): Control[];
|
|
91
159
|
export declare function stateKey(route: string | undefined, controls: readonly Control[]): string;
|
|
92
|
-
export declare function explore(port: ExplorePort, opts:
|
|
93
|
-
maxActions: number;
|
|
94
|
-
fillValue: (label: string) => string;
|
|
95
|
-
}): Promise<ExploreReport>;
|
|
160
|
+
export declare function explore(port: ExplorePort, opts: ExploreOptions): Promise<ExploreReport>;
|
|
96
161
|
/**
|
|
97
162
|
* A fresh start page, then the path. False when a step's control is gone or refused. Every state
|
|
98
163
|
* is reached this way, which is what makes returning to one a replay rather than a hope.
|
|
@@ -100,4 +165,12 @@ export declare function explore(port: ExplorePort, opts: {
|
|
|
100
165
|
export declare function replayPath(port: ExplorePort, path: readonly Step[], act: (ref: string, action: 'click' | 'fill', value?: string) => Promise<{
|
|
101
166
|
ok: boolean;
|
|
102
167
|
}>, spent: () => boolean): Promise<boolean>;
|
|
168
|
+
/**
|
|
169
|
+
* One action that sent the same write two or more times, whatever the server answered. A second
|
|
170
|
+
* copy sent after the server refused the credentials of the first is the client's retry, not a
|
|
171
|
+
* duplicate: the first did no work.
|
|
172
|
+
*/
|
|
173
|
+
export declare function noteDuplicateWrites(events: readonly ReticleEvent[], control: string, anomalies: ExploreReport['anomalies']): void;
|
|
174
|
+
/** The faults in an action's events: page errors, requests at or above `floor`, contradictions. */
|
|
175
|
+
export declare function noteAnomalies(events: readonly ReticleEvent[], control: string, anomalies: ExploreReport['anomalies'], floor?: number): void;
|
|
103
176
|
export {};
|
|
@@ -13,20 +13,36 @@
|
|
|
13
13
|
* - Controls that differ only by a number — the rows of a table — are one control. It is covered
|
|
14
14
|
* once, which is what keeps a 500-row page from eating the whole budget.
|
|
15
15
|
* - Text fields on a state are filled before its buttons are tried, so a submit is driven with
|
|
16
|
-
* input rather than dismissed as dead because it refused an empty form.
|
|
16
|
+
* input rather than dismissed as dead because it refused an empty form. Each field gets its own
|
|
17
|
+
* value (`fillValue` is told the field's position), so two fields a build swaps do not read alike.
|
|
18
|
+
* - `paths` keeps every click it tried, also a click that kept the same state (a count went up).
|
|
19
|
+
* A caller that reads the page after each path needs those clicks too.
|
|
17
20
|
*
|
|
18
21
|
* ponytail: breadth-first with a flat action budget, and every edge replays its path from a fresh
|
|
19
22
|
* load. That is O(states × path length), correct and slow; add prefix sharing if it proves too slow.
|
|
20
23
|
* A control that needs a choice (select, combobox) is not driven — it needs a value model.
|
|
21
24
|
*/
|
|
22
|
-
import { asNumber, asString, isAbsenceDerived, isAdvisory, parseInteractive, } from '@reticlehq/core';
|
|
25
|
+
import { EventType, asNumber, asString, isAbsenceDerived, isAdvisory, parseInteractive, } from '@reticlehq/core';
|
|
23
26
|
import { findContradictions } from '@reticlehq/engine/disagreement/contradictions.js';
|
|
24
|
-
import { controlKey, failedRequests, isConsoleError, writeKeyOf } from './session-fold.js';
|
|
27
|
+
import { controlKey, failedRequests, isActivity, isConsoleError, legitimatelyInert, writeKeyOf, } from './session-fold.js';
|
|
28
|
+
/** The attributes `fieldAttrs` reads: what a field takes, for its edge-case values. */
|
|
29
|
+
export const FIELD_ATTRS = [
|
|
30
|
+
'type',
|
|
31
|
+
'min',
|
|
32
|
+
'max',
|
|
33
|
+
'maxlength',
|
|
34
|
+
'minlength',
|
|
35
|
+
'pattern',
|
|
36
|
+
'required',
|
|
37
|
+
'inputmode',
|
|
38
|
+
];
|
|
25
39
|
/** Roles that take typed input. */
|
|
26
40
|
const TYPED = /^(?:textbox|searchbox|spinbutton)\b/;
|
|
27
41
|
/** Roles that need a value model this explorer does not have, and are left alone. */
|
|
28
42
|
const CHOOSER = /^(?:combobox|listbox|option|slider)\b/;
|
|
29
43
|
const FAILED_STATUS = 400;
|
|
44
|
+
/** The lowest status of an answer that is not an error. 0 is a request that never completed. */
|
|
45
|
+
const OK_MIN = 200;
|
|
30
46
|
/** A route without its query: `?__reticle_opened=1` and friends are not a different page. */
|
|
31
47
|
const pathOf = (route) => route?.split('?')[0];
|
|
32
48
|
export function controlsOf(tree) {
|
|
@@ -36,6 +52,7 @@ export function controlsOf(tree) {
|
|
|
36
52
|
ref: item.ref,
|
|
37
53
|
key: controlKey(item.desc),
|
|
38
54
|
holdsValue: /\[value="[^"]+"/.test(item.desc),
|
|
55
|
+
desc: item.desc,
|
|
39
56
|
}));
|
|
40
57
|
}
|
|
41
58
|
export function stateKey(route, controls) {
|
|
@@ -52,6 +69,8 @@ export async function explore(port, opts) {
|
|
|
52
69
|
const explored = new Set();
|
|
53
70
|
const queued = new Set();
|
|
54
71
|
const queue = [{ path: [] }];
|
|
72
|
+
const paths = [];
|
|
73
|
+
const surfaces = [];
|
|
55
74
|
const spent = () => actions >= opts.maxActions;
|
|
56
75
|
const act = async (ref, action, value) => {
|
|
57
76
|
actions += 1;
|
|
@@ -60,32 +79,47 @@ export async function explore(port, opts) {
|
|
|
60
79
|
const goTo = (path) => replayPath(port, path, act, spent);
|
|
61
80
|
while (queue.length > 0 && !spent()) {
|
|
62
81
|
const node = queue.shift();
|
|
63
|
-
if (node === undefined)
|
|
82
|
+
if (node === undefined) {
|
|
64
83
|
break;
|
|
84
|
+
}
|
|
65
85
|
if (!(await goTo(node.path))) {
|
|
66
|
-
if (!spent())
|
|
86
|
+
if (!spent()) {
|
|
67
87
|
unreachable += 1;
|
|
88
|
+
}
|
|
68
89
|
continue;
|
|
69
90
|
}
|
|
70
91
|
const here = await port.look();
|
|
71
92
|
const controls = controlsOf(here.tree);
|
|
72
93
|
const key = stateKey(here.route, controls);
|
|
73
|
-
if (explored.has(key))
|
|
94
|
+
if (explored.has(key)) {
|
|
74
95
|
continue;
|
|
96
|
+
}
|
|
75
97
|
explored.add(key);
|
|
76
98
|
const where = pathOf(here.route);
|
|
77
|
-
if (where !== undefined)
|
|
99
|
+
if (where !== undefined) {
|
|
78
100
|
routes.add(where);
|
|
79
|
-
|
|
101
|
+
}
|
|
102
|
+
for (const c of controls) {
|
|
80
103
|
seen.add(c.key);
|
|
104
|
+
}
|
|
81
105
|
// Only EMPTY fields. The first drive of the fixture typed placeholders over the login form's
|
|
82
106
|
// correct pre-filled credentials, got a 401, and never left the first page.
|
|
107
|
+
const fields = [...new Set(controls.filter((c) => TYPED.test(c.key)).map((c) => c.key))];
|
|
83
108
|
const typed = controls.filter((c) => TYPED.test(c.key) && !c.holdsValue);
|
|
84
|
-
const fills = typed.map((c) => ({
|
|
109
|
+
const fills = typed.map((c, index) => ({
|
|
85
110
|
key: c.key,
|
|
86
111
|
action: 'fill',
|
|
87
|
-
value: opts.fillValue(c.key),
|
|
112
|
+
value: opts.fillValue(c.key, index),
|
|
88
113
|
}));
|
|
114
|
+
const attrs = {};
|
|
115
|
+
// Every field, the pre-filled ones too: the explorer leaves a pre-filled field alone, but its
|
|
116
|
+
// edge values are still worth typing. A login form whose fields came filled got none.
|
|
117
|
+
for (const field of fields) {
|
|
118
|
+
const read = await port.fieldAttrs?.(field);
|
|
119
|
+
if (read !== undefined) {
|
|
120
|
+
attrs[field] = read;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
89
123
|
// Fills first, then the targets: valid input can reveal the button that moves on (a Continue,
|
|
90
124
|
// a Submit), and a list drawn before the fill would never contain it.
|
|
91
125
|
const fillHere = async () => {
|
|
@@ -100,31 +134,69 @@ export async function explore(port, opts) {
|
|
|
100
134
|
return 0 < fills.length ? controlsOf((await port.look()).tree) : before;
|
|
101
135
|
};
|
|
102
136
|
const ready = await fillHere();
|
|
103
|
-
for (const c of ready)
|
|
137
|
+
for (const c of ready) {
|
|
104
138
|
seen.add(c.key);
|
|
139
|
+
}
|
|
105
140
|
// Every control of THIS state, once. Not filtered by what other states touched: a "Save" on
|
|
106
141
|
// another screen is another control, and skipping it left reachable writes undriven.
|
|
107
142
|
const untried = [
|
|
108
143
|
...new Set(ready.filter((c) => !TYPED.test(c.key) && !CHOOSER.test(c.key)).map((c) => c.key)),
|
|
109
144
|
];
|
|
110
|
-
|
|
111
|
-
|
|
145
|
+
const surface = {
|
|
146
|
+
path: node.path,
|
|
147
|
+
fills,
|
|
148
|
+
fields,
|
|
149
|
+
attrs,
|
|
150
|
+
order: untried,
|
|
151
|
+
requests: [],
|
|
152
|
+
writes: [],
|
|
153
|
+
inert: [],
|
|
154
|
+
};
|
|
155
|
+
surfaces.push(surface);
|
|
156
|
+
// Controls no state has tried yet go first. Every page of an app repeats its nav, and trying the
|
|
157
|
+
// nav first on every state spent the budget before a page's own controls were pressed. Each
|
|
158
|
+
// control of the state is still tried; only the order changes.
|
|
159
|
+
const tryOrder = [
|
|
160
|
+
...untried.filter((c) => !touched.has(c)),
|
|
161
|
+
...untried.filter((c) => touched.has(c)),
|
|
162
|
+
];
|
|
163
|
+
for (const [index, target] of tryOrder.entries()) {
|
|
164
|
+
if (spent()) {
|
|
112
165
|
break;
|
|
166
|
+
}
|
|
113
167
|
// The first control is tried on the page already loaded and filled; every other one starts
|
|
114
168
|
// over from the recorded path, because the one before it may have changed or left this state.
|
|
115
|
-
if (index > 0 && !(await goTo(node.path)))
|
|
169
|
+
if (index > 0 && !(await goTo(node.path))) {
|
|
116
170
|
break;
|
|
171
|
+
}
|
|
117
172
|
const current = 0 === index ? ready : await fillHere();
|
|
118
173
|
const control = current.find((c) => c.key === target);
|
|
119
|
-
if (control === undefined || spent())
|
|
174
|
+
if (control === undefined || spent()) {
|
|
120
175
|
continue;
|
|
176
|
+
}
|
|
121
177
|
const result = await act(control.ref, 'click');
|
|
122
178
|
touched.add(target);
|
|
123
179
|
const path = [...node.path, ...fills, { key: target, action: 'click' }];
|
|
180
|
+
paths.push(path);
|
|
181
|
+
await opts.wait?.();
|
|
124
182
|
const after = await port.look();
|
|
125
183
|
const next = stateKey(after.route, controlsOf(after.tree));
|
|
126
184
|
noteWrites(result.events, path, key, next, writes);
|
|
185
|
+
if (result.events.some((e) => EventType.NET_REQUEST === e.type)) {
|
|
186
|
+
surface.requests.push(target);
|
|
187
|
+
}
|
|
188
|
+
if (result.events.some((e) => writeKeyOf(e) !== undefined)) {
|
|
189
|
+
surface.writes.push(target);
|
|
190
|
+
}
|
|
127
191
|
noteAnomalies(result.events, target, anomalies);
|
|
192
|
+
noteDuplicateWrites(result.events, target, anomalies);
|
|
193
|
+
if (result.ok &&
|
|
194
|
+
next === key &&
|
|
195
|
+
pathOf(after.route) === where &&
|
|
196
|
+
!result.events.some(isActivity) &&
|
|
197
|
+
!legitimatelyInert(control.desc)) {
|
|
198
|
+
surface.inert.push(target);
|
|
199
|
+
}
|
|
128
200
|
if (!explored.has(next) && !queued.has(next)) {
|
|
129
201
|
queued.add(next);
|
|
130
202
|
queue.push({ path, key: next });
|
|
@@ -142,6 +214,8 @@ export async function explore(port, opts) {
|
|
|
142
214
|
frontier: queue.filter((n) => n.key === undefined || !explored.has(n.key)).length,
|
|
143
215
|
unreachable,
|
|
144
216
|
budgetExhausted: spent(),
|
|
217
|
+
paths,
|
|
218
|
+
surfaces,
|
|
145
219
|
};
|
|
146
220
|
}
|
|
147
221
|
/**
|
|
@@ -149,47 +223,107 @@ export async function explore(port, opts) {
|
|
|
149
223
|
* is reached this way, which is what makes returning to one a replay rather than a hope.
|
|
150
224
|
*/
|
|
151
225
|
export async function replayPath(port, path, act, spent) {
|
|
152
|
-
if (!(await port.reset()))
|
|
226
|
+
if (!(await port.reset())) {
|
|
153
227
|
return false;
|
|
228
|
+
}
|
|
154
229
|
for (const step of path) {
|
|
155
|
-
if (spent())
|
|
230
|
+
if (spent()) {
|
|
156
231
|
return false;
|
|
232
|
+
}
|
|
157
233
|
const target = controlsOf((await port.look()).tree).find((c) => c.key === step.key);
|
|
158
|
-
if (target === undefined)
|
|
234
|
+
if (target === undefined) {
|
|
159
235
|
return false;
|
|
160
|
-
|
|
236
|
+
}
|
|
237
|
+
if (!(await act(target.ref, step.action, step.value)).ok) {
|
|
161
238
|
return false;
|
|
239
|
+
}
|
|
162
240
|
}
|
|
163
241
|
return true;
|
|
164
242
|
}
|
|
165
243
|
function noteWrites(events, path, beforeState, successState, writes) {
|
|
166
244
|
for (const event of events) {
|
|
167
245
|
const key = writeKeyOf(event);
|
|
168
|
-
if (key === undefined || writes.has(key))
|
|
246
|
+
if (key === undefined || writes.has(key)) {
|
|
169
247
|
continue;
|
|
248
|
+
}
|
|
170
249
|
const [method = '', urlPath = ''] = key.split(' ');
|
|
171
250
|
const url = event.data['url'];
|
|
172
251
|
const concrete = 'string' === typeof url ? new URL(url, 'http://localhost').pathname : urlPath;
|
|
173
|
-
|
|
252
|
+
const status = asNumber(event.data['status']);
|
|
253
|
+
const failed = status === undefined || status < OK_MIN || status >= FAILED_STATUS;
|
|
254
|
+
writes.set(key, {
|
|
255
|
+
key,
|
|
256
|
+
method,
|
|
257
|
+
urlPath: concrete,
|
|
258
|
+
path,
|
|
259
|
+
beforeState,
|
|
260
|
+
successState,
|
|
261
|
+
...(failed ? { failed } : {}),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** HTTP answers that refuse the credentials. A client sends the write again after one by design. */
|
|
266
|
+
const AUTH_REFUSED = new Set([401, 403]);
|
|
267
|
+
const DUPLICATE_REQUEST = 'duplicate-request';
|
|
268
|
+
/**
|
|
269
|
+
* One action that sent the same write two or more times, whatever the server answered. A second
|
|
270
|
+
* copy sent after the server refused the credentials of the first is the client's retry, not a
|
|
271
|
+
* duplicate: the first did no work.
|
|
272
|
+
*/
|
|
273
|
+
export function noteDuplicateWrites(events, control, anomalies) {
|
|
274
|
+
// Each request once, by its id: a write still in flight has only its `net.pending` here, and one
|
|
275
|
+
// that answered has both. A slow write sent twice is still two sends.
|
|
276
|
+
const sent = new Map();
|
|
277
|
+
const retried = new Set();
|
|
278
|
+
for (const [index, e] of events.entries()) {
|
|
279
|
+
const key = sentWriteKeyOf(e);
|
|
280
|
+
if (key === undefined) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (AUTH_REFUSED.has(asNumber(e.data['status']) ?? 0)) {
|
|
284
|
+
retried.add(key);
|
|
285
|
+
}
|
|
286
|
+
const id = asString(e.data['id']) ?? `#${String(index)}`;
|
|
287
|
+
sent.set(key, (sent.get(key) ?? new Set()).add(id));
|
|
288
|
+
}
|
|
289
|
+
for (const [key, ids] of sent) {
|
|
290
|
+
if (ids.size > 1 && !retried.has(key)) {
|
|
291
|
+
anomalies.push({
|
|
292
|
+
kind: DUPLICATE_REQUEST,
|
|
293
|
+
control,
|
|
294
|
+
detail: `one press sent ${key} ${String(ids.size)} times`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
174
297
|
}
|
|
175
298
|
}
|
|
176
|
-
|
|
299
|
+
/** The write a request or a request still in flight sends, as `writeKeyOf` spells it. */
|
|
300
|
+
function sentWriteKeyOf(e) {
|
|
301
|
+
if (EventType.NET_PENDING === e.type) {
|
|
302
|
+
return writeKeyOf({ ...e, type: EventType.NET_REQUEST });
|
|
303
|
+
}
|
|
304
|
+
return writeKeyOf(e);
|
|
305
|
+
}
|
|
306
|
+
/** The faults in an action's events: page errors, requests at or above `floor`, contradictions. */
|
|
307
|
+
export function noteAnomalies(events, control, anomalies, floor = FAILED_STATUS) {
|
|
177
308
|
const list = [...events];
|
|
178
|
-
for (const e of list.filter(isConsoleError))
|
|
309
|
+
for (const e of list.filter(isConsoleError)) {
|
|
179
310
|
anomalies.push({
|
|
180
311
|
kind: 'console-error',
|
|
181
312
|
control,
|
|
182
313
|
detail: asString(e.data['message']) ?? e.type,
|
|
183
314
|
});
|
|
184
|
-
|
|
315
|
+
}
|
|
316
|
+
for (const e of failedRequests(list, floor)) {
|
|
185
317
|
anomalies.push({
|
|
186
318
|
kind: 'failed-request',
|
|
187
319
|
control,
|
|
188
320
|
detail: `${asString(e.data['method']) ?? ''} ${asString(e.data['url']) ?? ''} → ${String(asNumber(e.data['status']) ?? '')}`,
|
|
189
321
|
});
|
|
322
|
+
}
|
|
190
323
|
for (const c of findContradictions(list, { actionSince: 0 })) {
|
|
191
|
-
if (isAdvisory(c.kind) || isAbsenceDerived(c.kind))
|
|
324
|
+
if (isAdvisory(c.kind) || isAbsenceDerived(c.kind)) {
|
|
192
325
|
continue;
|
|
326
|
+
}
|
|
193
327
|
anomalies.push({ kind: c.kind, control, detail: `${c.claim}, but ${c.counter}` });
|
|
194
328
|
}
|
|
195
329
|
}
|
|
@@ -25,6 +25,11 @@ export interface FailureBranches {
|
|
|
25
25
|
* by hand, or to give the app a visible outcome.
|
|
26
26
|
*/
|
|
27
27
|
indistinguishable: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Writes that answered an error while exploring too, so there is no success to compare with. Not a
|
|
30
|
+
* finding. A login with its API down lands on its error message both times.
|
|
31
|
+
*/
|
|
32
|
+
notSucceeded: string[];
|
|
28
33
|
/** Why nothing was driven, when nothing could be. */
|
|
29
34
|
skipped?: string;
|
|
30
35
|
}
|
|
@@ -24,10 +24,16 @@ function aborted(e, key) {
|
|
|
24
24
|
(0 === asNumber(e.data['status']) || e.data['error'] !== undefined));
|
|
25
25
|
}
|
|
26
26
|
export async function driveFailureBranches(port, writes, opts) {
|
|
27
|
-
const out = {
|
|
27
|
+
const out = {
|
|
28
|
+
branched: [],
|
|
29
|
+
unhandled: [],
|
|
30
|
+
indistinguishable: [],
|
|
31
|
+
notSucceeded: [],
|
|
32
|
+
};
|
|
28
33
|
const mock = port.mock;
|
|
29
|
-
if (mock === undefined)
|
|
34
|
+
if (mock === undefined) {
|
|
30
35
|
return { ...out, ...(0 < writes.length ? { skipped: NO_MOCKS } : {}) };
|
|
36
|
+
}
|
|
31
37
|
let actions = 0;
|
|
32
38
|
const spent = () => actions >= opts.maxActions;
|
|
33
39
|
const act = (ref, action, value) => {
|
|
@@ -35,28 +41,34 @@ export async function driveFailureBranches(port, writes, opts) {
|
|
|
35
41
|
return port.act(ref, action, value);
|
|
36
42
|
};
|
|
37
43
|
for (const write of writes) {
|
|
38
|
-
if (spent())
|
|
44
|
+
if (spent()) {
|
|
39
45
|
break;
|
|
46
|
+
}
|
|
40
47
|
const last = write.path.at(-1);
|
|
41
|
-
if (last === undefined)
|
|
48
|
+
if (last === undefined) {
|
|
42
49
|
continue;
|
|
50
|
+
}
|
|
43
51
|
try {
|
|
44
52
|
// Everything up to the step that fires the write, THEN the break. Installed before the replay,
|
|
45
53
|
// the abort also broke any setup step sending the same request, and the replay died short of
|
|
46
54
|
// the action it existed to reach.
|
|
47
|
-
if (!(await replayPath(port, write.path.slice(0, -1), act, spent)))
|
|
55
|
+
if (!(await replayPath(port, write.path.slice(0, -1), act, spent))) {
|
|
48
56
|
continue;
|
|
49
|
-
|
|
57
|
+
}
|
|
58
|
+
if (!(await mock([{ urlContains: write.urlPath, method: write.method, abort: true }]))) {
|
|
50
59
|
return { ...out, skipped: NO_MOCKS };
|
|
60
|
+
}
|
|
51
61
|
const control = controlsOf((await port.look()).tree).find((c) => c.key === last.key);
|
|
52
|
-
if (control === undefined)
|
|
62
|
+
if (control === undefined) {
|
|
53
63
|
continue;
|
|
64
|
+
}
|
|
54
65
|
const result = await act(control.ref, last.action, last.value);
|
|
55
66
|
// Driven means the write was SENT and did not complete. An action that never dispatched, or a
|
|
56
67
|
// request the rule did not match, drove nothing — and judging where the app landed after a
|
|
57
68
|
// write that succeeded would accuse it of claiming success over a failure that never happened.
|
|
58
|
-
if (!result.ok || !result.events.some((e) => aborted(e, write.key)))
|
|
69
|
+
if (!result.ok || !result.events.some((e) => aborted(e, write.key))) {
|
|
59
70
|
continue;
|
|
71
|
+
}
|
|
60
72
|
out.branched.push(write.key);
|
|
61
73
|
const after = await port.look();
|
|
62
74
|
const reached = stateKey(after.route, controlsOf(after.tree));
|
|
@@ -68,21 +80,27 @@ export async function driveFailureBranches(port, writes, opts) {
|
|
|
68
80
|
actionSince: 0,
|
|
69
81
|
expectedFailures: [{ method: write.method, urlContains: write.urlPath }],
|
|
70
82
|
}).find((c) => !isAdvisory(c.kind) && !isAbsenceDerived(c.kind));
|
|
71
|
-
if (contradiction !== undefined)
|
|
83
|
+
if (contradiction !== undefined) {
|
|
72
84
|
out.unhandled.push({
|
|
73
85
|
key: write.key,
|
|
74
86
|
detail: `${contradiction.claim}, but ${contradiction.counter}`,
|
|
75
87
|
});
|
|
88
|
+
}
|
|
76
89
|
// Landing where success lands only ACCUSES when success visibly moved the app. A write whose
|
|
77
90
|
// success shows nothing looks identical failing, and the first version of this called that
|
|
78
91
|
// "claimed success" — found by driving the fixture, whose add-item panel claims nothing at all.
|
|
79
|
-
else if (
|
|
92
|
+
else if (true === write.failed) {
|
|
93
|
+
out.notSucceeded.push(write.key);
|
|
94
|
+
}
|
|
95
|
+
else if (write.successState === write.beforeState) {
|
|
80
96
|
out.indistinguishable.push(write.key);
|
|
81
|
-
|
|
97
|
+
}
|
|
98
|
+
else if (reached === write.successState) {
|
|
82
99
|
out.unhandled.push({
|
|
83
100
|
key: write.key,
|
|
84
101
|
detail: `with ${write.key} failing, the app arrived exactly where it goes when the write succeeds (${after.route ?? 'same page'}) — it claimed success over a failure`,
|
|
85
102
|
});
|
|
103
|
+
}
|
|
86
104
|
}
|
|
87
105
|
finally {
|
|
88
106
|
await mock([]);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour checks that need no reference answer.
|
|
3
|
+
*
|
|
4
|
+
* A test needs someone to write down what the app should do. These do not: each one compares the app
|
|
5
|
+
* with itself — the page before an action with the page after its undo, the first press with the
|
|
6
|
+
* second, the value sent with the value shown, one run with an identical run. Each is a pure
|
|
7
|
+
* function over observations the explorer already takes, and each returns findings with the steps
|
|
8
|
+
* that show the fault and, when the app gives it, the source line of the element.
|
|
9
|
+
*
|
|
10
|
+
* A check that cannot decide returns nothing. A false finding costs more than a missed one, so every
|
|
11
|
+
* check below names the case it lets pass on purpose.
|
|
12
|
+
*/
|
|
13
|
+
import { type EdgeStep } from './edge-cases.js';
|
|
14
|
+
import type { ExploredWrite } from './explorer.js';
|
|
15
|
+
import type { FailureBranches } from './failure-branches.js';
|
|
16
|
+
import type { BehaviourFinding } from './ledger.js';
|
|
17
|
+
/** The rule each check holds the app to, in words, for the report. */
|
|
18
|
+
export declare const Invariant: {
|
|
19
|
+
readonly UNDO_RESTORES: "undo or cancel returns the previous page";
|
|
20
|
+
readonly REPEAT_IS_STABLE: "a repeated action does not change state again";
|
|
21
|
+
readonly RELOAD_KEEPS: "a saved value is still shown after reload";
|
|
22
|
+
readonly SHOWN_IS_SENT: "the value shown after a write is the value the write sent";
|
|
23
|
+
readonly DETERMINISTIC: "two identical drives give the same result";
|
|
24
|
+
readonly FAILURE_HANDLED: "no success over a failed write";
|
|
25
|
+
readonly NO_FAULT: "an edge case gets an answer, not an error";
|
|
26
|
+
readonly RESPONDS: "a control that is pressed does something";
|
|
27
|
+
};
|
|
28
|
+
export type Invariant = (typeof Invariant)[keyof typeof Invariant];
|
|
29
|
+
/** A page as the explorer looks at it: the route and the interactive snapshot tree. */
|
|
30
|
+
export interface PageObservation {
|
|
31
|
+
route?: string;
|
|
32
|
+
tree: string;
|
|
33
|
+
}
|
|
34
|
+
/** A write an action sent, with the server's answer. */
|
|
35
|
+
export interface WriteObservation {
|
|
36
|
+
/** `METHOD /path`, as `writeKeyOf` spells it. */
|
|
37
|
+
key: string;
|
|
38
|
+
status?: number;
|
|
39
|
+
/** The response body, as text. */
|
|
40
|
+
response?: string;
|
|
41
|
+
}
|
|
42
|
+
/** What every check takes besides its observations. */
|
|
43
|
+
interface Context {
|
|
44
|
+
steps: readonly EdgeStep[];
|
|
45
|
+
/** The element's source. Read it from an action result with `sourceOf` (surface/tools). */
|
|
46
|
+
source?: {
|
|
47
|
+
file: string;
|
|
48
|
+
line: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** The two pages show the same thing: refs and focus aside, every line and the route are equal. */
|
|
52
|
+
export declare const samePage: (a: PageObservation, b: PageObservation) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* (a) After an action and its undo or cancel, the page shows what it showed before the action.
|
|
55
|
+
* `after` is the page after the undo. Refs and focus are ignored; every other line counts.
|
|
56
|
+
*/
|
|
57
|
+
export declare function checkUndo(input: {
|
|
58
|
+
before: PageObservation;
|
|
59
|
+
after: PageObservation;
|
|
60
|
+
} & Context): BehaviourFinding[];
|
|
61
|
+
/**
|
|
62
|
+
* (b) An action pressed twice. Two faults:
|
|
63
|
+
* - the page after the second press is the page after the first, but the second press sent a POST
|
|
64
|
+
* the server accepted and answered differently: it created a second thing the screen hides;
|
|
65
|
+
* - the first press changed nothing on the page and the second changed it.
|
|
66
|
+
* Lets pass: a toggle or a counter (each press shows its own change), and a POST the server answers
|
|
67
|
+
* the same way twice (an upsert).
|
|
68
|
+
*/
|
|
69
|
+
export declare function checkRepeat(input: {
|
|
70
|
+
before: PageObservation;
|
|
71
|
+
afterFirst: PageObservation;
|
|
72
|
+
afterSecond: PageObservation;
|
|
73
|
+
firstWrites?: readonly WriteObservation[];
|
|
74
|
+
secondWrites?: readonly WriteObservation[];
|
|
75
|
+
} & Context): BehaviourFinding[];
|
|
76
|
+
/**
|
|
77
|
+
* (c) A value the app said it saved is still shown after a reload. Lets pass: an empty value (there
|
|
78
|
+
* is nothing to look for), and an app that said the value was not saved.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkReloadKeeps(input: {
|
|
81
|
+
value: string;
|
|
82
|
+
afterReload: PageObservation;
|
|
83
|
+
appSaidNotSaved?: boolean;
|
|
84
|
+
} & Context): BehaviourFinding[];
|
|
85
|
+
/**
|
|
86
|
+
* (d) The value a field shows after a write is the value the write request sent for it. Lets pass:
|
|
87
|
+
* a field that shows nothing (it was cleared or closed), and differences of padding, case and digit
|
|
88
|
+
* grouping.
|
|
89
|
+
*/
|
|
90
|
+
export declare function checkShownMatchesSent(input: {
|
|
91
|
+
field: string;
|
|
92
|
+
sent: string;
|
|
93
|
+
shown: string | undefined;
|
|
94
|
+
} & Context): BehaviourFinding[];
|
|
95
|
+
/**
|
|
96
|
+
* (e) Two identical drives from a fresh page show the same page. A difference is reported as
|
|
97
|
+
* intermittent: one run does not say which result is the fault.
|
|
98
|
+
*
|
|
99
|
+
* ponytail: every digit is masked before the compare, so a clock, an id or a relative time does not
|
|
100
|
+
* read as a different result. The ceiling: two runs that differ only in a number ("3 items" against
|
|
101
|
+
* "4 items") pass. Mask only time-like and id-like tokens if that proves too loose.
|
|
102
|
+
*/
|
|
103
|
+
export declare function checkDeterministic(input: {
|
|
104
|
+
first: PageObservation;
|
|
105
|
+
second: PageObservation;
|
|
106
|
+
} & Context): BehaviourFinding[];
|
|
107
|
+
/**
|
|
108
|
+
* (f) No success over a failed write. The judgement is `driveFailureBranches`'s own; this only gives
|
|
109
|
+
* each unhandled write the steps that fire it, so it reads like every other finding.
|
|
110
|
+
*/
|
|
111
|
+
export declare function failureFindings(branches: FailureBranches, writes: readonly ExploredWrite[]): BehaviourFinding[];
|
|
112
|
+
export {};
|