@reticlehq/server 3.9.0 → 3.10.1
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 +43 -2
- 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/init.mdx +2 -2
- 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 +30 -1
- package/docs/usage.md +17 -14
- package/package.json +11 -14
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -0,0 +1,188 @@
|
|
|
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 { BehaviourFindingKind } from '@reticlehq/core';
|
|
14
|
+
import { describeStep } from './edge-cases.js';
|
|
15
|
+
/** The rule each check holds the app to, in words, for the report. */
|
|
16
|
+
export const Invariant = {
|
|
17
|
+
UNDO_RESTORES: 'undo or cancel returns the previous page',
|
|
18
|
+
REPEAT_IS_STABLE: 'a repeated action does not change state again',
|
|
19
|
+
RELOAD_KEEPS: 'a saved value is still shown after reload',
|
|
20
|
+
SHOWN_IS_SENT: 'the value shown after a write is the value the write sent',
|
|
21
|
+
DETERMINISTIC: 'two identical drives give the same result',
|
|
22
|
+
FAILURE_HANDLED: 'no success over a failed write',
|
|
23
|
+
NO_FAULT: 'an edge case gets an answer, not an error',
|
|
24
|
+
RESPONDS: 'a control that is pressed does something',
|
|
25
|
+
};
|
|
26
|
+
const REF = /\s*\(ref=e\d+\)/g;
|
|
27
|
+
/** Focus moves with every action and is not part of what the page shows. */
|
|
28
|
+
const FOCUSED = /\[focused\]|,focused\b|\bfocused,/g;
|
|
29
|
+
const DIGITS = /\d+/g;
|
|
30
|
+
/** How many changed lines a finding quotes. */
|
|
31
|
+
const MAX_QUOTED = 3;
|
|
32
|
+
/** Writes that create something new each time they are sent. PUT and DELETE are idempotent. */
|
|
33
|
+
const CREATES = 'POST ';
|
|
34
|
+
const OK_MIN = 200;
|
|
35
|
+
const OK_MAX = 299;
|
|
36
|
+
function visibleLines(page) {
|
|
37
|
+
const lines = page.tree
|
|
38
|
+
.split('\n')
|
|
39
|
+
.map((line) => line
|
|
40
|
+
.replace(REF, '')
|
|
41
|
+
.replace(FOCUSED, '')
|
|
42
|
+
.replace(/\s*\[\]/g, '')
|
|
43
|
+
.trimEnd())
|
|
44
|
+
.filter((line) => line.trim().length > 0);
|
|
45
|
+
return [`route ${page.route ?? ''}`, ...lines];
|
|
46
|
+
}
|
|
47
|
+
const same = (a, b) => a.length === b.length && a.every((line, i) => line === b[i]);
|
|
48
|
+
/** The two pages show the same thing: refs and focus aside, every line and the route are equal. */
|
|
49
|
+
export const samePage = (a, b) => same(visibleLines(a), visibleLines(b));
|
|
50
|
+
/** The lines one page has and the other does not, quoted for a finding. */
|
|
51
|
+
function difference(before, after) {
|
|
52
|
+
const gone = before.filter((line) => !after.includes(line)).slice(0, MAX_QUOTED);
|
|
53
|
+
const added = after.filter((line) => !before.includes(line)).slice(0, MAX_QUOTED);
|
|
54
|
+
const parts = [
|
|
55
|
+
...(gone.length > 0 ? [`missing: ${gone.map((l) => l.trim()).join('; ')}`] : []),
|
|
56
|
+
...(added.length > 0 ? [`new: ${added.map((l) => l.trim()).join('; ')}`] : []),
|
|
57
|
+
];
|
|
58
|
+
return parts.length > 0 ? parts.join(' | ') : 'the lines are in a different order';
|
|
59
|
+
}
|
|
60
|
+
function finding(kind, invariant, detail, ctx, extraSteps = []) {
|
|
61
|
+
const src = ctx.source;
|
|
62
|
+
return {
|
|
63
|
+
kind,
|
|
64
|
+
invariant,
|
|
65
|
+
detail,
|
|
66
|
+
sequence: [...ctx.steps.map(describeStep), ...extraSteps],
|
|
67
|
+
...(src === undefined ? {} : { source: `${src.file}:${String(src.line)}` }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* (a) After an action and its undo or cancel, the page shows what it showed before the action.
|
|
72
|
+
* `after` is the page after the undo. Refs and focus are ignored; every other line counts.
|
|
73
|
+
*/
|
|
74
|
+
export function checkUndo(input) {
|
|
75
|
+
const before = visibleLines(input.before);
|
|
76
|
+
const after = visibleLines(input.after);
|
|
77
|
+
if (same(before, after)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
finding(BehaviourFindingKind.UNDO_NOT_RESTORED, Invariant.UNDO_RESTORES, `the page after undo is not the page before the action (${difference(before, after)})`, input),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
const isOk = (w) => w.status !== undefined && w.status >= OK_MIN && w.status <= OK_MAX;
|
|
85
|
+
/**
|
|
86
|
+
* (b) An action pressed twice. Two faults:
|
|
87
|
+
* - the page after the second press is the page after the first, but the second press sent a POST
|
|
88
|
+
* the server accepted and answered differently: it created a second thing the screen hides;
|
|
89
|
+
* - the first press changed nothing on the page and the second changed it.
|
|
90
|
+
* Lets pass: a toggle or a counter (each press shows its own change), and a POST the server answers
|
|
91
|
+
* the same way twice (an upsert).
|
|
92
|
+
*/
|
|
93
|
+
export function checkRepeat(input) {
|
|
94
|
+
const before = visibleLines(input.before);
|
|
95
|
+
const first = visibleLines(input.afterFirst);
|
|
96
|
+
const second = visibleLines(input.afterSecond);
|
|
97
|
+
if (same(first, second)) {
|
|
98
|
+
const firstWrites = input.firstWrites ?? [];
|
|
99
|
+
const hidden = (input.secondWrites ?? []).find((w) => w.key.startsWith(CREATES) &&
|
|
100
|
+
isOk(w) &&
|
|
101
|
+
!firstWrites.some((f) => f.key === w.key && f.response === w.response));
|
|
102
|
+
if (hidden === undefined) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return [
|
|
106
|
+
finding(BehaviourFindingKind.REPEAT_CHANGED_STATE, Invariant.REPEAT_IS_STABLE, `the second press sent ${hidden.key} and the server accepted it with a new answer, but the page shows the same result as after the first press`, input),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
if (!same(before, first)) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
return [
|
|
113
|
+
finding(BehaviourFindingKind.REPEAT_CHANGED_STATE, Invariant.REPEAT_IS_STABLE, `the first press changed nothing and the second press changed the page (${difference(first, second)})`, input),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* (c) A value the app said it saved is still shown after a reload. Lets pass: an empty value (there
|
|
118
|
+
* is nothing to look for), and an app that said the value was not saved.
|
|
119
|
+
*/
|
|
120
|
+
export function checkReloadKeeps(input) {
|
|
121
|
+
const value = input.value.trim();
|
|
122
|
+
if (0 === value.length ||
|
|
123
|
+
true === input.appSaidNotSaved ||
|
|
124
|
+
input.afterReload.tree.includes(value)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
finding(BehaviourFindingKind.LOST_AFTER_RELOAD, Invariant.RELOAD_KEEPS, `the app saved ${JSON.stringify(value)}, and after a reload the page does not show it`, input, ['reload']),
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
/** Padding, case and digit grouping are presentation, not a different value. */
|
|
132
|
+
function normalized(value) {
|
|
133
|
+
const text = value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
134
|
+
return /^-?[\d,]+(?:\.\d+)?$/.test(text) ? text.replace(/,/g, '') : text;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* (d) The value a field shows after a write is the value the write request sent for it. Lets pass:
|
|
138
|
+
* a field that shows nothing (it was cleared or closed), and differences of padding, case and digit
|
|
139
|
+
* grouping.
|
|
140
|
+
*/
|
|
141
|
+
export function checkShownMatchesSent(input) {
|
|
142
|
+
if (input.shown === undefined || 0 === input.shown.trim().length) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
if (normalized(input.shown) === normalized(input.sent)) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return [
|
|
149
|
+
finding(BehaviourFindingKind.SHOWN_DIFFERS_FROM_SENT, Invariant.SHOWN_IS_SENT, `${input.field} shows ${JSON.stringify(input.shown)}, but the write sent ${JSON.stringify(input.sent)}`, input),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* (e) Two identical drives from a fresh page show the same page. A difference is reported as
|
|
154
|
+
* intermittent: one run does not say which result is the fault.
|
|
155
|
+
*
|
|
156
|
+
* ponytail: every digit is masked before the compare, so a clock, an id or a relative time does not
|
|
157
|
+
* read as a different result. The ceiling: two runs that differ only in a number ("3 items" against
|
|
158
|
+
* "4 items") pass. Mask only time-like and id-like tokens if that proves too loose.
|
|
159
|
+
*/
|
|
160
|
+
export function checkDeterministic(input) {
|
|
161
|
+
const shape = (page) => visibleLines(page).map((line) => line.replace(DIGITS, '#'));
|
|
162
|
+
const first = shape(input.first);
|
|
163
|
+
const second = shape(input.second);
|
|
164
|
+
if (same(first, second)) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
...finding(BehaviourFindingKind.INTERMITTENT_RESULT, Invariant.DETERMINISTIC, `two identical drives showed different pages (${difference(first, second)})`, input),
|
|
170
|
+
intermittent: true,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* (f) No success over a failed write. The judgement is `driveFailureBranches`'s own; this only gives
|
|
176
|
+
* each unhandled write the steps that fire it, so it reads like every other finding.
|
|
177
|
+
*/
|
|
178
|
+
export function failureFindings(branches, writes) {
|
|
179
|
+
return branches.unhandled.map((u) => ({
|
|
180
|
+
kind: BehaviourFindingKind.FAILURE_UNHANDLED,
|
|
181
|
+
invariant: Invariant.FAILURE_HANDLED,
|
|
182
|
+
detail: u.detail,
|
|
183
|
+
sequence: [
|
|
184
|
+
...(writes.find((w) => w.key === u.key)?.path ?? []).map(describeStep),
|
|
185
|
+
`fail ${u.key}`,
|
|
186
|
+
],
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
@@ -30,6 +30,18 @@ export declare const CoverageLevel: {
|
|
|
30
30
|
readonly EXECUTED: "executed";
|
|
31
31
|
};
|
|
32
32
|
export type CoverageLevel = (typeof CoverageLevel)[keyof typeof CoverageLevel];
|
|
33
|
+
/** How many missing items a level names before it only counts them. */
|
|
34
|
+
export declare const MAX_MISSING = 50;
|
|
35
|
+
/** A behaviour fault the explorer found. See `invariants.ts`, which produces these. */
|
|
36
|
+
export declare const BehaviourFindingSchema: z.ZodObject<{
|
|
37
|
+
kind: z.ZodString;
|
|
38
|
+
invariant: z.ZodString;
|
|
39
|
+
detail: z.ZodString;
|
|
40
|
+
sequence: z.ZodArray<z.ZodString>;
|
|
41
|
+
source: z.ZodOptional<z.ZodString>;
|
|
42
|
+
intermittent: z.ZodOptional<z.ZodBoolean>;
|
|
43
|
+
}, z.core.$strip>;
|
|
44
|
+
export type BehaviourFinding = z.infer<typeof BehaviourFindingSchema>;
|
|
33
45
|
declare const LedgerSchema: z.ZodObject<{
|
|
34
46
|
routes: z.ZodObject<{
|
|
35
47
|
discovered: z.ZodArray<z.ZodString>;
|
|
@@ -48,6 +60,17 @@ declare const LedgerSchema: z.ZodObject<{
|
|
|
48
60
|
branched: z.ZodArray<z.ZodString>;
|
|
49
61
|
unhandled: z.ZodArray<z.ZodString>;
|
|
50
62
|
}, z.core.$strip>;
|
|
63
|
+
edges: z.ZodDefault<z.ZodObject<{
|
|
64
|
+
probed: z.ZodArray<z.ZodString>;
|
|
65
|
+
}, z.core.$strip>>;
|
|
66
|
+
findings: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
67
|
+
kind: z.ZodString;
|
|
68
|
+
invariant: z.ZodString;
|
|
69
|
+
detail: z.ZodString;
|
|
70
|
+
sequence: z.ZodArray<z.ZodString>;
|
|
71
|
+
source: z.ZodOptional<z.ZodString>;
|
|
72
|
+
intermittent: z.ZodOptional<z.ZodBoolean>;
|
|
73
|
+
}, z.core.$strip>>>;
|
|
51
74
|
code: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
52
75
|
name: z.ZodString;
|
|
53
76
|
executed: z.ZodBoolean;
|
|
@@ -62,6 +85,8 @@ export interface LedgerDelta {
|
|
|
62
85
|
controls?: Partial<AppLedger['controls']>;
|
|
63
86
|
journeys?: Partial<AppLedger['journeys']>;
|
|
64
87
|
writes?: Partial<AppLedger['writes']>;
|
|
88
|
+
edges?: Partial<AppLedger['edges']>;
|
|
89
|
+
findings?: readonly BehaviourFinding[];
|
|
65
90
|
code?: CodeCoverage;
|
|
66
91
|
/** When `code` was taken, so a file edited since reads as not measured rather than as covered. */
|
|
67
92
|
codeTakenAt?: number;
|
|
@@ -87,6 +112,49 @@ export declare function coveragePercents(reticleRoot: string): Record<string, nu
|
|
|
87
112
|
export declare function emptyLedger(): AppLedger;
|
|
88
113
|
export declare function mergeLedger(into: AppLedger, delta: LedgerDelta): AppLedger;
|
|
89
114
|
export declare function levelsOf(ledger: AppLedger): LevelReport[];
|
|
115
|
+
/**
|
|
116
|
+
* What was never reached, as named items: the holes in the exploration.
|
|
117
|
+
*
|
|
118
|
+
* The levels above give a number per level. A number does not say where to go next, and it hides a
|
|
119
|
+
* level that was never measured behind an empty list. `holesOf` names every gap by kind, says when
|
|
120
|
+
* a kind was not measured at all, and counts the items past its cap. Nothing is left out in silence.
|
|
121
|
+
*/
|
|
122
|
+
export declare const HoleKind: {
|
|
123
|
+
/** Routes discovered and never visited. */
|
|
124
|
+
readonly ROUTES: "routes";
|
|
125
|
+
/** Controls seen and never driven. */
|
|
126
|
+
readonly CONTROLS: "controls";
|
|
127
|
+
/** Controls seen whose edge cases were never driven. */
|
|
128
|
+
readonly EDGES: "edges";
|
|
129
|
+
/** Writes seen whose failure path was never driven. */
|
|
130
|
+
readonly WRITES: "writes";
|
|
131
|
+
/** App functions loaded and never executed, as `file: function`. */
|
|
132
|
+
readonly FUNCTIONS: "functions";
|
|
133
|
+
/** Components the app has and never rendered. */
|
|
134
|
+
readonly COMPONENTS: "components";
|
|
135
|
+
};
|
|
136
|
+
export type HoleKind = (typeof HoleKind)[keyof typeof HoleKind];
|
|
137
|
+
export interface HoleList {
|
|
138
|
+
/** False when nothing could measure this kind: an empty list then means "unknown", not "none". */
|
|
139
|
+
measured: boolean;
|
|
140
|
+
items: string[];
|
|
141
|
+
/** How many more items there are than `items` names. */
|
|
142
|
+
overflow?: number;
|
|
143
|
+
/** Why the kind is not measured, or what to do to measure it. */
|
|
144
|
+
note?: string;
|
|
145
|
+
}
|
|
146
|
+
export type HolesReport = Record<HoleKind, HoleList>;
|
|
147
|
+
/** The React adapter's component inventory: every component it knows, and the ones it rendered. */
|
|
148
|
+
export interface ComponentInventory {
|
|
149
|
+
known: readonly string[];
|
|
150
|
+
rendered: readonly string[];
|
|
151
|
+
}
|
|
152
|
+
export declare const HoleNote: {
|
|
153
|
+
readonly CODE: "Not measured: code coverage needs a browser Reticle drives (`reticle drive`, or RETICLE_CDP_URL).";
|
|
154
|
+
readonly COMPONENTS: "Not measured: no component inventory is available. A component that never rendered still shows in `functions` when code coverage is on.";
|
|
155
|
+
readonly EDGES: "No edge cases were driven yet. Run `reticle_crawl { exhaustive: true }`; every control listed here is still a hole.";
|
|
156
|
+
};
|
|
157
|
+
export declare function holesOf(ledger: AppLedger, components?: ComponentInventory): HolesReport;
|
|
90
158
|
/** The best percentage each level has reached, raised to today's where today is higher. */
|
|
91
159
|
export declare function raiseBest(ledger: AppLedger): AppLedger;
|
|
92
160
|
/**
|
|
@@ -34,13 +34,31 @@ export const CoverageLevel = {
|
|
|
34
34
|
};
|
|
35
35
|
const LEDGER_VERSION = 1;
|
|
36
36
|
/** How many missing items a level names before it only counts them. */
|
|
37
|
-
const MAX_MISSING = 50;
|
|
37
|
+
export const MAX_MISSING = 50;
|
|
38
38
|
const names = z.array(z.string());
|
|
39
|
+
/** A behaviour fault the explorer found. See `invariants.ts`, which produces these. */
|
|
40
|
+
export const BehaviourFindingSchema = z.object({
|
|
41
|
+
/** A `BehaviourFindingKind` from core, or the kind of a fault an edge case caused (`console-error`). */
|
|
42
|
+
kind: z.string(),
|
|
43
|
+
/** The rule that did not hold, in words. */
|
|
44
|
+
invariant: z.string(),
|
|
45
|
+
detail: z.string(),
|
|
46
|
+
/** The steps that show it, from a fresh page. */
|
|
47
|
+
sequence: z.array(z.string()),
|
|
48
|
+
/** `file:line` of the element, when the app gives source attribution. */
|
|
49
|
+
source: z.string().optional(),
|
|
50
|
+
/** True when the fault did not occur on every run. */
|
|
51
|
+
intermittent: z.boolean().optional(),
|
|
52
|
+
});
|
|
39
53
|
const LedgerSchema = z.object({
|
|
40
54
|
routes: z.object({ discovered: names, reached: names }),
|
|
41
55
|
controls: z.object({ seen: names, touched: names, proved: names }),
|
|
42
56
|
journeys: z.object({ completed: names }),
|
|
43
57
|
writes: z.object({ seen: names, branched: names, unhandled: names }),
|
|
58
|
+
/** Controls whose edge cases (edge inputs, edge interactions) were driven. */
|
|
59
|
+
edges: z.object({ probed: names }).default({ probed: [] }),
|
|
60
|
+
/** Behaviour faults, one copy each. Absent in a ledger from before they existed. */
|
|
61
|
+
findings: z.array(BehaviourFindingSchema).default([]),
|
|
44
62
|
code: z.record(z.string(), z.record(z.string(), z.object({ name: z.string(), executed: z.boolean() }))),
|
|
45
63
|
/** Per file, when its code coverage was last taken. Absent in a ledger from before it existed. */
|
|
46
64
|
codeTaken: z.record(z.string(), z.number()).default({}),
|
|
@@ -74,8 +92,9 @@ export function coveragePercents(reticleRoot) {
|
|
|
74
92
|
}
|
|
75
93
|
const out = {};
|
|
76
94
|
for (const report of levelsOf(parseLedger(text))) {
|
|
77
|
-
if (report.pct !== undefined)
|
|
95
|
+
if (report.pct !== undefined) {
|
|
78
96
|
out[report.level] = report.pct;
|
|
97
|
+
}
|
|
79
98
|
}
|
|
80
99
|
return 0 < Object.keys(out).length ? out : undefined;
|
|
81
100
|
}
|
|
@@ -85,12 +104,27 @@ export function emptyLedger() {
|
|
|
85
104
|
controls: { seen: [], touched: [], proved: [] },
|
|
86
105
|
journeys: { completed: [] },
|
|
87
106
|
writes: { seen: [], branched: [], unhandled: [] },
|
|
107
|
+
edges: { probed: [] },
|
|
108
|
+
findings: [],
|
|
88
109
|
code: {},
|
|
89
110
|
codeTaken: {},
|
|
90
111
|
best: {},
|
|
91
112
|
};
|
|
92
113
|
}
|
|
93
114
|
const union = (a, b) => b === undefined ? [...a] : [...new Set([...a, ...b])];
|
|
115
|
+
/** The same fault found again by the same steps is one finding; the first copy is kept. */
|
|
116
|
+
const findingKey = (f) => `${f.kind}\n${f.sequence.join('\n')}`;
|
|
117
|
+
function unionFindings(into, add) {
|
|
118
|
+
const out = [...into];
|
|
119
|
+
const keys = new Set(out.map(findingKey));
|
|
120
|
+
for (const f of add ?? []) {
|
|
121
|
+
if (!keys.has(findingKey(f))) {
|
|
122
|
+
keys.add(findingKey(f));
|
|
123
|
+
out.push(f);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
94
128
|
export function mergeLedger(into, delta) {
|
|
95
129
|
const code = { ...into.code };
|
|
96
130
|
const codeTaken = { ...into.codeTaken };
|
|
@@ -100,12 +134,15 @@ export function mergeLedger(into, delta) {
|
|
|
100
134
|
merged[key] = { name: fn.name, executed: fn.executed || true === merged[key]?.executed };
|
|
101
135
|
}
|
|
102
136
|
code[file] = merged;
|
|
103
|
-
if (delta.codeTakenAt !== undefined)
|
|
137
|
+
if (delta.codeTakenAt !== undefined) {
|
|
104
138
|
codeTaken[file] = delta.codeTakenAt;
|
|
139
|
+
}
|
|
105
140
|
}
|
|
106
|
-
// Reaching a route discovers it; touching or proving a control means it was seen.
|
|
141
|
+
// Reaching a route discovers it; touching or proving a control means it was seen. Probing a
|
|
142
|
+
// control's edge cases drove it, so it was touched.
|
|
107
143
|
const reached = union(into.routes.reached, delta.routes?.reached);
|
|
108
|
-
const
|
|
144
|
+
const probed = union(into.edges.probed, delta.edges?.probed);
|
|
145
|
+
const touched = union(union(into.controls.touched, delta.controls?.touched), probed);
|
|
109
146
|
const proved = union(into.controls.proved, delta.controls?.proved);
|
|
110
147
|
const branched = union(into.writes.branched, delta.writes?.branched);
|
|
111
148
|
return {
|
|
@@ -124,6 +161,8 @@ export function mergeLedger(into, delta) {
|
|
|
124
161
|
branched,
|
|
125
162
|
unhandled: union(into.writes.unhandled, delta.writes?.unhandled),
|
|
126
163
|
},
|
|
164
|
+
edges: { probed },
|
|
165
|
+
findings: unionFindings(into.findings, delta.findings),
|
|
127
166
|
code,
|
|
128
167
|
codeTaken,
|
|
129
168
|
best: { ...into.best },
|
|
@@ -166,12 +205,74 @@ export function levelsOf(ledger) {
|
|
|
166
205
|
executedLevel(ledger.code),
|
|
167
206
|
];
|
|
168
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* What was never reached, as named items: the holes in the exploration.
|
|
210
|
+
*
|
|
211
|
+
* The levels above give a number per level. A number does not say where to go next, and it hides a
|
|
212
|
+
* level that was never measured behind an empty list. `holesOf` names every gap by kind, says when
|
|
213
|
+
* a kind was not measured at all, and counts the items past its cap. Nothing is left out in silence.
|
|
214
|
+
*/
|
|
215
|
+
export const HoleKind = {
|
|
216
|
+
/** Routes discovered and never visited. */
|
|
217
|
+
ROUTES: 'routes',
|
|
218
|
+
/** Controls seen and never driven. */
|
|
219
|
+
CONTROLS: 'controls',
|
|
220
|
+
/** Controls seen whose edge cases were never driven. */
|
|
221
|
+
EDGES: 'edges',
|
|
222
|
+
/** Writes seen whose failure path was never driven. */
|
|
223
|
+
WRITES: 'writes',
|
|
224
|
+
/** App functions loaded and never executed, as `file: function`. */
|
|
225
|
+
FUNCTIONS: 'functions',
|
|
226
|
+
/** Components the app has and never rendered. */
|
|
227
|
+
COMPONENTS: 'components',
|
|
228
|
+
};
|
|
229
|
+
export const HoleNote = {
|
|
230
|
+
CODE: 'Not measured: code coverage needs a browser Reticle drives (`reticle drive`, or RETICLE_CDP_URL).',
|
|
231
|
+
COMPONENTS: 'Not measured: no component inventory is available. A component that never rendered still shows in `functions` when code coverage is on.',
|
|
232
|
+
EDGES: 'No edge cases were driven yet. Run `reticle_crawl { exhaustive: true }`; every control listed here is still a hole.',
|
|
233
|
+
};
|
|
234
|
+
function named(universe, covered) {
|
|
235
|
+
const done = new Set(covered);
|
|
236
|
+
const missing = [...new Set(universe)].filter((item) => !done.has(item));
|
|
237
|
+
return {
|
|
238
|
+
measured: true,
|
|
239
|
+
items: missing.slice(0, MAX_MISSING),
|
|
240
|
+
...(missing.length > MAX_MISSING ? { overflow: missing.length - MAX_MISSING } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** The same named list a coverage level reports, so the two can never disagree. */
|
|
244
|
+
function fromLevel(ledger, level) {
|
|
245
|
+
const report = levelsOf(ledger).find((l) => l.level === level);
|
|
246
|
+
return {
|
|
247
|
+
measured: true,
|
|
248
|
+
items: report?.missing ?? [],
|
|
249
|
+
...(report?.missingOverflow === undefined ? {} : { overflow: report.missingOverflow }),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
export function holesOf(ledger, components) {
|
|
253
|
+
const edges = named(ledger.controls.seen, ledger.edges.probed);
|
|
254
|
+
return {
|
|
255
|
+
[HoleKind.ROUTES]: fromLevel(ledger, CoverageLevel.REACHED),
|
|
256
|
+
[HoleKind.CONTROLS]: fromLevel(ledger, CoverageLevel.TOUCHED),
|
|
257
|
+
[HoleKind.EDGES]: 0 === ledger.edges.probed.length
|
|
258
|
+
? { ...edges, measured: false, note: HoleNote.EDGES }
|
|
259
|
+
: edges,
|
|
260
|
+
[HoleKind.WRITES]: fromLevel(ledger, CoverageLevel.BRANCHED),
|
|
261
|
+
[HoleKind.FUNCTIONS]: 0 === Object.keys(ledger.code).length
|
|
262
|
+
? { measured: false, items: [], note: HoleNote.CODE }
|
|
263
|
+
: fromLevel(ledger, CoverageLevel.EXECUTED),
|
|
264
|
+
[HoleKind.COMPONENTS]: components === undefined
|
|
265
|
+
? { measured: false, items: [], note: HoleNote.COMPONENTS }
|
|
266
|
+
: named(components.known, components.rendered),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
169
269
|
/** The best percentage each level has reached, raised to today's where today is higher. */
|
|
170
270
|
export function raiseBest(ledger) {
|
|
171
271
|
const best = { ...ledger.best };
|
|
172
272
|
for (const l of levelsOf(ledger)) {
|
|
173
|
-
if (l.pct !== undefined && l.pct > (best[l.level] ?? -1))
|
|
273
|
+
if (l.pct !== undefined && l.pct > (best[l.level] ?? -1)) {
|
|
174
274
|
best[l.level] = l.pct;
|
|
275
|
+
}
|
|
175
276
|
}
|
|
176
277
|
return { ...ledger, best };
|
|
177
278
|
}
|
|
@@ -181,9 +282,11 @@ export function raiseBest(ledger) {
|
|
|
181
282
|
*/
|
|
182
283
|
export function acceptCurrent(ledger) {
|
|
183
284
|
const best = {};
|
|
184
|
-
for (const l of levelsOf(ledger))
|
|
185
|
-
if (l.pct !== undefined)
|
|
285
|
+
for (const l of levelsOf(ledger)) {
|
|
286
|
+
if (l.pct !== undefined) {
|
|
186
287
|
best[l.level] = l.pct;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
187
290
|
return { ...ledger, best };
|
|
188
291
|
}
|
|
189
292
|
/** Levels now below the best they ever reached — something new appeared and nothing covered it. */
|
|
@@ -191,8 +294,9 @@ export function regressions(ledger) {
|
|
|
191
294
|
const out = [];
|
|
192
295
|
for (const l of levelsOf(ledger)) {
|
|
193
296
|
const was = ledger.best[l.level];
|
|
194
|
-
if (l.pct !== undefined && was !== undefined && l.pct < was)
|
|
297
|
+
if (l.pct !== undefined && was !== undefined && l.pct < was) {
|
|
195
298
|
out.push({ level: l.level, was, now: l.pct });
|
|
299
|
+
}
|
|
196
300
|
}
|
|
197
301
|
return out;
|
|
198
302
|
}
|
|
@@ -207,8 +311,9 @@ function servedAs(code, file) {
|
|
|
207
311
|
export function unexecutedChanged(code, changed) {
|
|
208
312
|
return changed.filter((file) => {
|
|
209
313
|
const served = servedAs(code, file);
|
|
210
|
-
if (served === undefined)
|
|
314
|
+
if (served === undefined) {
|
|
211
315
|
return false;
|
|
316
|
+
}
|
|
212
317
|
const fns = Object.values(code[served] ?? {});
|
|
213
318
|
return fns.length > 0 && fns.every((fn) => !fn.executed);
|
|
214
319
|
});
|
|
@@ -223,8 +328,9 @@ export function staleChanged(ledger, changed, modifiedAt) {
|
|
|
223
328
|
return changed.filter((file) => {
|
|
224
329
|
const served = servedAs(ledger.code, file);
|
|
225
330
|
const modified = modifiedAt(file);
|
|
226
|
-
if (served === undefined || modified === undefined)
|
|
331
|
+
if (served === undefined || modified === undefined) {
|
|
227
332
|
return false;
|
|
333
|
+
}
|
|
228
334
|
const taken = ledger.codeTaken[served];
|
|
229
335
|
return taken === undefined || modified > taken;
|
|
230
336
|
});
|
|
@@ -243,8 +349,9 @@ export class LedgerStore {
|
|
|
243
349
|
text = await this.#fs.readFile(this.#path);
|
|
244
350
|
}
|
|
245
351
|
catch (error) {
|
|
246
|
-
if (this.#fs.isNotFound(error))
|
|
352
|
+
if (this.#fs.isNotFound(error)) {
|
|
247
353
|
return emptyLedger();
|
|
354
|
+
}
|
|
248
355
|
throw error;
|
|
249
356
|
}
|
|
250
357
|
return parseLedger(text);
|
|
@@ -264,8 +371,9 @@ export class LedgerStore {
|
|
|
264
371
|
return withFileLock(this.#path, async () => {
|
|
265
372
|
const ledger = await this.load();
|
|
266
373
|
const accepted = regressions(ledger);
|
|
267
|
-
if (accepted.length > 0)
|
|
374
|
+
if (accepted.length > 0) {
|
|
268
375
|
await this.#save(acceptCurrent(ledger));
|
|
376
|
+
}
|
|
269
377
|
return accepted;
|
|
270
378
|
});
|
|
271
379
|
}
|
|
@@ -29,3 +29,13 @@ export declare function sessionDelta(input: {
|
|
|
29
29
|
export declare function isConsoleError(e: ReticleEvent): boolean;
|
|
30
30
|
/** Requests that answered at or above `floor`. */
|
|
31
31
|
export declare function failedRequests(events: ReticleEvent[], floor: number): ReticleEvent[];
|
|
32
|
+
/** Any buffer event that proves the app reacted to a click (vs a dead/no-op control). */
|
|
33
|
+
export declare function isActivity(e: ReticleEvent): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Controls whose correct behaviour IS to do nothing when clicked.
|
|
36
|
+
*
|
|
37
|
+
* Read off the same descriptor the crawler already has, so no extra round trip: `[disabled]` is inert
|
|
38
|
+
* by definition, an already `[checked]` radio/checkbox is idempotent, and clicking a text field
|
|
39
|
+
* focuses it rather than changing anything.
|
|
40
|
+
*/
|
|
41
|
+
export declare function legitimatelyInert(desc: string): boolean;
|
|
@@ -14,12 +14,14 @@ const ID_SEGMENT = /^(?:\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9
|
|
|
14
14
|
* refund of order 41 and of order 42 are one endpoint with one failure path to drive, not two.
|
|
15
15
|
*/
|
|
16
16
|
export function writeKeyOf(event) {
|
|
17
|
-
if (EventType.NET_REQUEST !== event.type)
|
|
17
|
+
if (EventType.NET_REQUEST !== event.type) {
|
|
18
18
|
return undefined;
|
|
19
|
+
}
|
|
19
20
|
const method = asString(event.data['method'])?.toUpperCase();
|
|
20
21
|
const url = asString(event.data['url']);
|
|
21
|
-
if (method === undefined || url === undefined || !MUTATING_METHODS.includes(method))
|
|
22
|
+
if (method === undefined || url === undefined || !MUTATING_METHODS.includes(method)) {
|
|
22
23
|
return undefined;
|
|
24
|
+
}
|
|
23
25
|
let path;
|
|
24
26
|
try {
|
|
25
27
|
path = new URL(url, 'http://localhost').pathname;
|
|
@@ -71,9 +73,50 @@ export function isConsoleError(e) {
|
|
|
71
73
|
/** Requests that answered at or above `floor`. */
|
|
72
74
|
export function failedRequests(events, floor) {
|
|
73
75
|
return events.filter((e) => {
|
|
74
|
-
if (e.type !== EventType.NET_REQUEST)
|
|
76
|
+
if (e.type !== EventType.NET_REQUEST) {
|
|
75
77
|
return false;
|
|
78
|
+
}
|
|
76
79
|
const status = asNumber(e.data['status']);
|
|
77
80
|
return status !== undefined && status >= floor;
|
|
78
81
|
});
|
|
79
82
|
}
|
|
83
|
+
/** Any buffer event that proves the app reacted to a click (vs a dead/no-op control). */
|
|
84
|
+
export function isActivity(e) {
|
|
85
|
+
return (e.type === EventType.NET_REQUEST ||
|
|
86
|
+
e.type === EventType.DOM_ADDED ||
|
|
87
|
+
e.type === EventType.DOM_REMOVED ||
|
|
88
|
+
e.type === EventType.DOM_ATTR ||
|
|
89
|
+
e.type === EventType.DOM_TEXT ||
|
|
90
|
+
e.type === EventType.ROUTE_CHANGE ||
|
|
91
|
+
e.type === EventType.SIGNAL ||
|
|
92
|
+
e.type === EventType.ANIM_START ||
|
|
93
|
+
e.type === EventType.ANIM_END);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Controls whose correct behaviour IS to do nothing when clicked.
|
|
97
|
+
*
|
|
98
|
+
* Read off the same descriptor the crawler already has, so no extra round trip: `[disabled]` is inert
|
|
99
|
+
* by definition, an already `[checked]` radio/checkbox is idempotent, and clicking a text field
|
|
100
|
+
* focuses it rather than changing anything.
|
|
101
|
+
*/
|
|
102
|
+
export function legitimatelyInert(desc) {
|
|
103
|
+
if (desc.includes('[disabled]')) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (desc.includes('[checked]')) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
// Inputs, and the ARIA LIVE-REGION roles.
|
|
110
|
+
//
|
|
111
|
+
// `alert`, `status`, `log`, `timer` and `marquee` exist to be READ — a screen reader announces
|
|
112
|
+
// them and nothing is meant to happen when one is clicked. Clicking twice and seeing nothing is
|
|
113
|
+
// therefore correct behaviour, and reporting it as `dead-control` accuses a working app.
|
|
114
|
+
//
|
|
115
|
+
// Found by driving a real app: the demo reported `dead-control — alert "Invalid email or
|
|
116
|
+
// password"`, which is a login form correctly telling somebody their password was wrong. That is
|
|
117
|
+
// the most expensive kind of false positive there is — the strongest claim the crawler makes,
|
|
118
|
+
// about an app that was working, and on the path a new user meets first.
|
|
119
|
+
//
|
|
120
|
+
// `alertdialog` is deliberately NOT here: it is interactive, and a word boundary keeps it out.
|
|
121
|
+
return /\b(textbox|searchbox|combobox|spinbutton|alert|status|log|timer|marquee)\b/.test(desc);
|
|
122
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the successor before it answers — the same wait `navigate { reload }` does.
|
|
8
8
|
*/
|
|
9
9
|
import { type CommandResult, type ReticleEvent } from '@reticlehq/core';
|
|
10
|
-
import type
|
|
10
|
+
import { type ExplorePort } from './explorer.js';
|
|
11
11
|
export interface PortSession {
|
|
12
12
|
readonly id: string;
|
|
13
13
|
readonly url: string;
|
|
@@ -17,6 +17,8 @@ export interface PortSession {
|
|
|
17
17
|
beginAction?(tool: string, args: Record<string, unknown>): unknown;
|
|
18
18
|
finishAction?(): void;
|
|
19
19
|
}
|
|
20
|
+
/** How long a press waits for the requests it started to answer. */
|
|
21
|
+
export declare const PENDING_WAIT_MS = 10000;
|
|
20
22
|
export declare function sessionPort(input: {
|
|
21
23
|
sessions: {
|
|
22
24
|
get(id: string): PortSession | undefined;
|