@reticlehq/server 3.5.0 → 3.6.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/README.md +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +10 -10
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +51 -12
- package/dist/command/cli/cloud-login.d.ts +8 -0
- package/dist/command/cli/cloud-login.js +36 -2
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +6 -4
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +28 -22
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
- package/dist/command/setup/bringup/dev-server-wait.js +19 -0
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +18 -1
- package/dist/command/setup/node-effects.js +2 -1
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +5 -0
- package/dist/command/setup/run-setup.js +35 -27
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -73
- package/dist/features/harness/fill-values.js +3 -169
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +76 -82
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -3
- package/dist/memory/cloud/sync-cycle.js +16 -3
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +80 -2
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
- package/dist/memory/project/artifact-root-resolver.js +94 -13
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +3 -1
- package/dist/portal/bridge/bridge.js +19 -2
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +3 -3
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +7 -2
- package/dist/portal/session/session.js +21 -18
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.js +11 -1
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +27 -13
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +23 -7
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +37 -4
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -206
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +52 -1
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +15 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +1 -1
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +13 -11
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +22 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +17 -7
- package/docs/telemetry.md +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -2
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -58
- package/dist/features/harness/openai-driver.js +0 -206
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { formatStepAddress } from 'open-verification';
|
|
2
2
|
import { assertsState, expectLabel, expectedElementTestid, withoutClauses } from '@reticlehq/core';
|
|
3
3
|
import { span } from '../../trace.js';
|
|
4
|
-
import { anchorLabel, expectElementDrift, resolveTestid, testidDrift,
|
|
4
|
+
import { ambiguityDrift, anchorLabel, expectElementDrift, reresolved, resolveTestid, testidDrift, } from './flow-anchor.js';
|
|
5
5
|
export { nearestIsAmbiguous, nearestTestid, resolveQuery } from './flow-anchor.js';
|
|
6
6
|
import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
|
|
7
|
+
import { evalConsole } from '@reticlehq/engine/question/predicate/predicate-console.js';
|
|
7
8
|
import { stepEffect } from '@reticlehq/engine/evidence/step-effect.js';
|
|
8
|
-
import { AnchorKind, DriftReason, EventType, FlowStepTool, StepEffect, PredicateKind, } from '@reticlehq/core';
|
|
9
|
+
import { AnchorKind, DriftReason, EventType, FlowStepTool, StepEffect, PredicateKind, ReplayStatus, } from '@reticlehq/core';
|
|
9
10
|
import { asString, isConsequenceDrift } from '@reticlehq/core';
|
|
10
11
|
import { anchorFieldName } from './fields/flow-secret-field.js';
|
|
11
12
|
import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
|
|
@@ -100,11 +101,48 @@ function summarizeConsequence(events) {
|
|
|
100
101
|
const status = 'number' === typeof data['status'] ? ` ${data['status']}` : '';
|
|
101
102
|
parts.push(`${method} ${path}${status}`.trim());
|
|
102
103
|
}
|
|
103
|
-
const errors = events.filter(
|
|
104
|
+
const errors = events.filter(isConsoleError).length;
|
|
104
105
|
if (errors > 0)
|
|
105
106
|
parts.push(`${errors} console error${errors > 1 ? 's' : ''}`);
|
|
106
107
|
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
107
108
|
}
|
|
109
|
+
const isConsoleError = (e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT;
|
|
110
|
+
/** At most this many console errors travel on one step; its `digest` still counts them all. */
|
|
111
|
+
const CONSOLE_ERRORS_PER_STEP = 5;
|
|
112
|
+
/** The console errors a step's window saw, for the run artifact's evidence. See FlowStepResult. */
|
|
113
|
+
function consoleErrorsIn(events) {
|
|
114
|
+
const errors = events
|
|
115
|
+
.filter(isConsoleError)
|
|
116
|
+
.slice(0, CONSOLE_ERRORS_PER_STEP)
|
|
117
|
+
.map((e) => ({ level: e.type, message: asString(e.data['message']) ?? '', at: e.t }));
|
|
118
|
+
return errors.length > 0 ? errors : undefined;
|
|
119
|
+
}
|
|
120
|
+
/** The console-absent clauses an expect cannot pass without: itself, or any `allOf` member. */
|
|
121
|
+
function requiredConsoleAbsences(expect) {
|
|
122
|
+
if (PredicateKind.ALL_OF === expect.kind)
|
|
123
|
+
return expect.predicates.flatMap(requiredConsoleAbsences);
|
|
124
|
+
return PredicateKind.CONSOLE === expect.kind && true === expect.absent ? [expect] : [];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A console-absent expect, read again over the step's WHOLE window.
|
|
128
|
+
*
|
|
129
|
+
* The wait that graded the expect ends before the window does, so an error landing between the two
|
|
130
|
+
* was counted by the step's consequence and missed by its verdict (#1344). The window wins.
|
|
131
|
+
*/
|
|
132
|
+
function lateConsoleDrift(expect, events) {
|
|
133
|
+
for (const clause of requiredConsoleAbsences(expect)) {
|
|
134
|
+
const read = evalConsole(events, clause);
|
|
135
|
+
if (read.pass)
|
|
136
|
+
continue;
|
|
137
|
+
return {
|
|
138
|
+
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
139
|
+
reason: read.failureReason ?? "the step's declared consequence did not hold",
|
|
140
|
+
anchor: expectLabel(expect),
|
|
141
|
+
nearest: null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
108
146
|
/** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
|
|
109
147
|
export async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
|
|
110
148
|
const { refs, hint } = await resolveTestid(session, value, sleep);
|
|
@@ -124,17 +162,13 @@ export async function runTestidStep(session, step, index, value, dynamic, confir
|
|
|
124
162
|
* reads `drift` and `ok` and never reads a note, so "we guessed which element you meant" was
|
|
125
163
|
* indistinguishable from "it did what it did before" -- the one claim a replay makes.
|
|
126
164
|
*
|
|
127
|
-
* The
|
|
128
|
-
*
|
|
165
|
+
* The record itself comes from `ambiguityDrift`, shared with the role and component runners: this
|
|
166
|
+
* anchor kind was the first to get the rule, and writing it inline is how the other two came to
|
|
167
|
+
* answer the same question differently.
|
|
129
168
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
tool: step.tool,
|
|
134
|
-
anchor: value,
|
|
135
|
-
ok: false,
|
|
136
|
-
drift: ambiguousAnchorDrift(value, refs.length),
|
|
137
|
-
};
|
|
169
|
+
const ambiguous = ambiguityDrift(step.anchor, refs);
|
|
170
|
+
if (ambiguous !== null) {
|
|
171
|
+
return { step: index, tool: step.tool, anchor: value, ok: false, drift: ambiguous };
|
|
138
172
|
}
|
|
139
173
|
const ref = refs[0] ?? '';
|
|
140
174
|
/*
|
|
@@ -146,15 +180,12 @@ export async function runTestidStep(session, step, index, value, dynamic, confir
|
|
|
146
180
|
* so the kind most likely to appear in a real flow was the kind without the cure.
|
|
147
181
|
*
|
|
148
182
|
* The re-resolve keeps the ambiguity rule above: more than one match is drift, never a guess, so
|
|
149
|
-
* it
|
|
183
|
+
* it reports an `anchor_ambiguous` drift rather than handing back the first of the new matches.
|
|
150
184
|
*/
|
|
151
185
|
const result = await actOnResolvedRef(session, step, index, value, ref, confirmDangerous,
|
|
152
186
|
// The field this step types into — from the anchor, so a redacted fill can be supplied from
|
|
153
187
|
// RETICLE_SECRET_<FIELD> without the flow carrying the secret.
|
|
154
|
-
anchorFieldName(step.anchor), async () =>
|
|
155
|
-
const again = await resolveTestid(session, value, sleep);
|
|
156
|
-
return 1 === again.refs.length ? again.refs[0] : undefined;
|
|
157
|
-
});
|
|
188
|
+
anchorFieldName(step.anchor), async () => reresolved(step.anchor, (await resolveTestid(session, value, sleep)).refs));
|
|
158
189
|
if (!result.ok)
|
|
159
190
|
return result;
|
|
160
191
|
// assert the step's expect.element testid is present AFTER the action —
|
|
@@ -297,6 +328,19 @@ export function resumeIndex(from, steps) {
|
|
|
297
328
|
return undefined;
|
|
298
329
|
return steps.slice(0, at).some((step) => StepEffect.COMMITS === step.effect) ? 0 : at;
|
|
299
330
|
}
|
|
331
|
+
/** How far a replay drives: `to` stops before a step, `at` picks up at one. Positive indices only. */
|
|
332
|
+
export function replayWindow(args) {
|
|
333
|
+
const index = (raw) => 'number' === typeof raw && Number.isInteger(raw) && 0 < raw ? raw : undefined;
|
|
334
|
+
const to = index(args['to']);
|
|
335
|
+
const at = index(args['at']);
|
|
336
|
+
return { ...(to === undefined ? {} : { to }), ...(at === undefined ? {} : { at }) };
|
|
337
|
+
}
|
|
338
|
+
/** A part-way replay's status, from the steps it drove. */
|
|
339
|
+
export function partialStatus(steps) {
|
|
340
|
+
if (steps.some((step) => step.drift !== undefined))
|
|
341
|
+
return ReplayStatus.DRIFT;
|
|
342
|
+
return steps.every((step) => step.ok) ? ReplayStatus.OK : ReplayStatus.ERROR;
|
|
343
|
+
}
|
|
300
344
|
/** The `from` argument, narrowed: a step index or a step id. */
|
|
301
345
|
export function resumeArg(raw) {
|
|
302
346
|
return 'number' === typeof raw || ('string' === typeof raw && raw.length > 0) ? raw : undefined;
|
|
@@ -342,7 +386,8 @@ async function runInvokeStep(session, flow, step, index, waitForSignal, signalTi
|
|
|
342
386
|
}
|
|
343
387
|
// `from` is an offset into the CALLER's steps. A sub-journey invoked from the setup prefix runs
|
|
344
388
|
// whole and unchecked (from = its length); one invoked at or after the resume point runs normally.
|
|
345
|
-
|
|
389
|
+
// Nor the progress callback: a sub-flow's steps are one step of the flow the chip is drawing.
|
|
390
|
+
const { from: _outer, onStep: _progress, to: _to, at: _at, ...rest } = options;
|
|
346
391
|
const carried = setup ? { ...rest, from: sub.steps.length } : rest;
|
|
347
392
|
const nested = await replayFlow(session, sub, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, {
|
|
348
393
|
...carried,
|
|
@@ -377,13 +422,28 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
377
422
|
// wait for the slowest step would trade a false red for a slow suite. See FlowStep.timeoutMs.
|
|
378
423
|
const waitFor = (step) => step.timeoutMs ?? flow.signalTimeoutMs ?? signalTimeoutMs;
|
|
379
424
|
let index = 0;
|
|
425
|
+
const tell = (result) => {
|
|
426
|
+
try {
|
|
427
|
+
options.onStep?.(index + 1, flow.steps.length, true === result?.ok && result.drift === undefined);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
/* progress is a picture of the replay, never a reason to stop it */
|
|
431
|
+
}
|
|
432
|
+
};
|
|
380
433
|
try {
|
|
381
434
|
for (const step of flow.steps) {
|
|
435
|
+
if (options.to !== undefined && index >= options.to)
|
|
436
|
+
break;
|
|
437
|
+
if (options.at !== undefined && index < options.at) {
|
|
438
|
+
index += 1;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
382
441
|
if (step.invoke !== undefined) {
|
|
383
442
|
results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options, index < from));
|
|
384
443
|
const last = results[results.length - 1];
|
|
385
444
|
if (last !== undefined && index < from && last.ok)
|
|
386
445
|
results.pop();
|
|
446
|
+
tell(last);
|
|
387
447
|
if (last !== undefined && false === last.ok)
|
|
388
448
|
break;
|
|
389
449
|
index += 1;
|
|
@@ -459,6 +519,18 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
459
519
|
const consequence = summarizeConsequence(windowEvents);
|
|
460
520
|
if (consequence !== undefined)
|
|
461
521
|
result.consequence = consequence;
|
|
522
|
+
const consoleErrors = consoleErrorsIn(windowEvents);
|
|
523
|
+
if (consoleErrors !== undefined)
|
|
524
|
+
result.consoleErrors = consoleErrors;
|
|
525
|
+
for (const expectation of checked) {
|
|
526
|
+
if (!result.ok || result.drift !== undefined)
|
|
527
|
+
break;
|
|
528
|
+
const late = lateConsoleDrift(expectation, windowEvents);
|
|
529
|
+
if (late !== undefined) {
|
|
530
|
+
result.ok = false;
|
|
531
|
+
result.drift = late;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
462
534
|
// Per-step wall time is NOT shipped: it is `window.until - window.since`, computed from two numbers
|
|
463
535
|
// the next line already puts in the same object, under the same emission condition. A step used to
|
|
464
536
|
// carry the subtraction AND both operands, on every step of every replay.
|
|
@@ -484,6 +556,7 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
|
|
|
484
556
|
if (index < from)
|
|
485
557
|
result.note = `setup step for resuming at step ${String(from)} failed`;
|
|
486
558
|
results.push(result);
|
|
559
|
+
tell(result);
|
|
487
560
|
// Under `sweep`, a failure whose action still RAN does not stop the run — the page is where the
|
|
488
561
|
// step left it, so the next step is as meaningful as it was going to be. Anything else halts.
|
|
489
562
|
const sweepPast = true === options.sweep &&
|
|
@@ -27,7 +27,11 @@ export function buildFlowChips(flows, sessionProjectId) {
|
|
|
27
27
|
const start = first !== undefined && first.anchor.kind === AnchorKind.TESTID
|
|
28
28
|
? first.anchor.value
|
|
29
29
|
: undefined;
|
|
30
|
-
chips.push(
|
|
30
|
+
chips.push({
|
|
31
|
+
name: flow.name,
|
|
32
|
+
createdAt: flow.createdAt,
|
|
33
|
+
...(start === undefined ? {} : { start }),
|
|
34
|
+
});
|
|
31
35
|
}
|
|
32
36
|
return chips;
|
|
33
37
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { AnchorKind, type FlowAnchor, type FlowStep, type FlowStepResult, type Predicate } from '@reticlehq/core';
|
|
1
|
+
import { AnchorKind, type FlowAnchor, type FlowReplayResult, type FlowStep, type FlowStepResult, type Predicate } from '@reticlehq/core';
|
|
2
2
|
import type { FlowReplaySession, Sleep } from './flow-replay-types.js';
|
|
3
|
+
import { type Reresolved } from './flow-anchor.js';
|
|
3
4
|
export declare function runRoleStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
|
|
4
5
|
kind: typeof AnchorKind.ROLE;
|
|
5
6
|
}>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
|
|
@@ -19,8 +20,11 @@ field?: string,
|
|
|
19
20
|
* found, and the retry below is worthless without it: re-dispatching the SAME dead ref fails
|
|
20
21
|
* identically. What changed between the two attempts is the DOM, so the locator has to be run
|
|
21
22
|
* against it again.
|
|
23
|
+
*
|
|
24
|
+
* It reports WHAT it found, not just a ref: see `Reresolved`. A re-render that turns one match into
|
|
25
|
+
* three is a drift, and the caller is the only layer that can say so with the right anchor in hand.
|
|
22
26
|
*/
|
|
23
|
-
reresolve?: () => Promise<
|
|
27
|
+
reresolve?: () => Promise<Reresolved | undefined>): Promise<FlowStepResult>;
|
|
24
28
|
/** Run one component-anchored step: re-resolve via QUERY by:'component', ACT on the live ref, else drift. */
|
|
25
29
|
export declare function runComponentStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
|
|
26
30
|
kind: typeof AnchorKind.COMPONENT;
|
|
@@ -55,6 +59,20 @@ export declare function anchorQueryArgs(anchor: FlowAnchor): Record<string, unkn
|
|
|
55
59
|
* flow's own `requires` holds. No tool writes `requires`, so the reset's hint hands over this line.
|
|
56
60
|
*/
|
|
57
61
|
export declare function anchorPrecondition(anchor: FlowAnchor): Predicate | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The honest answer when a step's target kept going stale.
|
|
64
|
+
*
|
|
65
|
+
* Replay looks the element up again and goes on while that gets it further, so a step still stale
|
|
66
|
+
* after that is one the page re-rendered around faster than it could be targeted. Reporting it as
|
|
67
|
+
* `error` with the browser's own sentence says "your app broke", which is a claim nothing observed
|
|
68
|
+
* and sends a reader into product code that is fine. It is the same bucket as a document that went
|
|
69
|
+
* away: the steps before it are real and are attached, the step that could not be acted on is among
|
|
70
|
+
* them with its index, and nothing after it ran.
|
|
71
|
+
*
|
|
72
|
+
* Left alone, so the ordinary verdict stands, when anything else is in the run: a drifted anchor is
|
|
73
|
+
* drift whatever else happened, and a failure that is not staleness is a failure.
|
|
74
|
+
*/
|
|
75
|
+
export declare function staleTargetResult(name: string, steps: readonly FlowStepResult[]): FlowReplayResult | undefined;
|
|
58
76
|
/**
|
|
59
77
|
* Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
|
|
60
78
|
* ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* or a step's events land in the wrong window depending on how it was addressed.
|
|
8
8
|
*/
|
|
9
9
|
import { expectedElementTestid } from '@reticlehq/core';
|
|
10
|
-
import { AnchorKind, DEGRADED_ANCHOR_ROLE, DriftReason, PredicateKind, QueryBy, ReticleCommand, } from '@reticlehq/core';
|
|
10
|
+
import { AnchorKind, DEGRADED_ANCHOR_ROLE, DriftReason, PredicateKind, QueryBy, ReplayStatus, ReticleCommand, } from '@reticlehq/core';
|
|
11
11
|
import { ReticleTool } from '@reticlehq/core';
|
|
12
12
|
import { replayActionArgs } from './replay.js';
|
|
13
13
|
import { isStaleRefError } from '../../surface/tools/act/act-sequence-retry.js';
|
|
14
14
|
import { waitForReaction } from '../../surface/tools/act/react-grace.js';
|
|
15
15
|
import { anchorFieldName } from './flows.js';
|
|
16
|
-
import { anchorLabel, componentLabel, componentQueryArgs, expectElementDrift, resolveQuery, testidDrift, } from './flow-anchor.js';
|
|
16
|
+
import { ambiguityDrift, refFor, anchorLabel, componentLabel, componentQueryArgs, expectElementDrift, reresolved, resolveQuery, testidDrift, } from './flow-anchor.js';
|
|
17
17
|
import { nearestRoleName } from './role-anchor-nearest.js';
|
|
18
18
|
import { roleDriftReason } from '../../judgement/outcome/role-drift-reason.js';
|
|
19
19
|
/** Query args for a NAMED role anchor — the handle that identifies an instance, not a JSX site. */
|
|
@@ -63,7 +63,25 @@ async function nearestRoleNameOnPage(session, anchor) {
|
|
|
63
63
|
export async function runRoleStep(session, step, index, anchor, confirmDangerous, sleep) {
|
|
64
64
|
const label = `${anchor.role} "${String(anchor.name)}"`;
|
|
65
65
|
const { refs } = await resolveQuery(session, roleQueryArgs(anchor), sleep);
|
|
66
|
-
|
|
66
|
+
/*
|
|
67
|
+
* More than one match is DRIFT, not a guess. This path took `refs[0]`, dispatched, and returned
|
|
68
|
+
* `ok: true`, so a flow recorded against "View result" on row 3 of a table replayed by clicking row
|
|
69
|
+
* 1 and PASSED whenever every row shared the consequence (the same GET returning 200). The verdict
|
|
70
|
+
* reads `drift` and `ok` and nothing else, so a green here is indistinguishable from a replay that
|
|
71
|
+
* did what it did before.
|
|
72
|
+
*
|
|
73
|
+
* A role+name anchor is MORE likely to land here than a testid is: `button "View result"` is
|
|
74
|
+
* user-visible text, and a table repeats it by nature, so several matches is the expected shape of
|
|
75
|
+
* a recorded row action rather than an accident.
|
|
76
|
+
*
|
|
77
|
+
* The check itself lives in `ambiguityDrift`, shared with the testid and component runners: written
|
|
78
|
+
* per-runner, this rule got applied to two anchor kinds and forgotten for the third.
|
|
79
|
+
*/
|
|
80
|
+
const ambiguous = ambiguityDrift(anchor, refs);
|
|
81
|
+
if (ambiguous !== null) {
|
|
82
|
+
return { step: index, tool: step.tool, anchor: label, ok: false, drift: ambiguous };
|
|
83
|
+
}
|
|
84
|
+
const ref = refFor(anchor, refs);
|
|
67
85
|
if (ref === undefined) {
|
|
68
86
|
// `nearest` used to be the literal null, so heal answered "no nearest match cleared the
|
|
69
87
|
// confidence floor" for EVERY role-anchored drift — a structural limit reported as a judgement
|
|
@@ -86,8 +104,15 @@ export async function runRoleStep(session, step, index, anchor, confirmDangerous
|
|
|
86
104
|
};
|
|
87
105
|
}
|
|
88
106
|
return await actOnResolvedRef(session, step, index, label, ref, confirmDangerous, anchorFieldName(anchor),
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
/*
|
|
108
|
+
* The retry's whole point: run the SAME locator against the DOM as it is now.
|
|
109
|
+
*
|
|
110
|
+
* It reports WHAT it found, not just a ref. A re-render can turn an unambiguous anchor into an
|
|
111
|
+
* ambiguous one between the two dispatches, and dispatching at the first of the new matches would
|
|
112
|
+
* re-make the guess the check above exists to refuse — one layer deeper, where the caller cannot
|
|
113
|
+
* see it. `reresolved` is the same rule as that check, so the two cannot disagree.
|
|
114
|
+
*/
|
|
115
|
+
async () => reresolved(anchor, (await resolveQuery(session, roleQueryArgs(anchor), sleep)).refs));
|
|
91
116
|
}
|
|
92
117
|
/**
|
|
93
118
|
* `assertActionAllowed`'s refusal — "potentially destructive action blocked; retry with
|
|
@@ -131,6 +156,9 @@ field,
|
|
|
131
156
|
* found, and the retry below is worthless without it: re-dispatching the SAME dead ref fails
|
|
132
157
|
* identically. What changed between the two attempts is the DOM, so the locator has to be run
|
|
133
158
|
* against it again.
|
|
159
|
+
*
|
|
160
|
+
* It reports WHAT it found, not just a ref: see `Reresolved`. A re-render that turns one match into
|
|
161
|
+
* three is a drift, and the caller is the only layer that can say so with the right anchor in hand.
|
|
134
162
|
*/
|
|
135
163
|
reresolve) {
|
|
136
164
|
const dispatch = async (at) => {
|
|
@@ -167,8 +195,24 @@ reresolve) {
|
|
|
167
195
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
168
196
|
});
|
|
169
197
|
const fresh = await reresolve();
|
|
198
|
+
/*
|
|
199
|
+
* The re-render turned one match into SEVERAL, so there is nothing to dispatch at and the answer
|
|
200
|
+
* is a drift — the same `anchor_ambiguous` the first resolution would have given, reported for the
|
|
201
|
+
* same reason.
|
|
202
|
+
*
|
|
203
|
+
* The staleness this retry exists to absorb is a different diagnosis with a different fix, and
|
|
204
|
+
* this used to be the only one reported: the step came back as `{ok: false, error: "ref 'e-orig'
|
|
205
|
+
* no longer resolves to an element"}` — a sentence about an element that is GONE, sent for an
|
|
206
|
+
* element that is now three. An agent reading it goes hunting for a rename, or re-records the
|
|
207
|
+
* flow, when the actual fix is a narrower anchor. Telling the two apart is what `Reresolved` is
|
|
208
|
+
* for, and the reason `ANCHOR_AMBIGUOUS` exists as its own kind rather than folding into "not
|
|
209
|
+
* found".
|
|
210
|
+
*/
|
|
211
|
+
if (fresh !== undefined && 'drift' in fresh) {
|
|
212
|
+
return { step: index, tool: step.tool, anchor: label, ok: false, drift: fresh.drift };
|
|
213
|
+
}
|
|
170
214
|
if (fresh !== undefined)
|
|
171
|
-
act = await dispatch(fresh);
|
|
215
|
+
act = await dispatch(fresh.ref);
|
|
172
216
|
}
|
|
173
217
|
const result = { step: index, tool: step.tool, anchor: label, ok: act.ok };
|
|
174
218
|
if (!act.ok)
|
|
@@ -195,10 +239,28 @@ export async function runComponentStep(session, step, index, anchor, confirmDang
|
|
|
195
239
|
},
|
|
196
240
|
};
|
|
197
241
|
}
|
|
242
|
+
/*
|
|
243
|
+
* Several matches is DRIFT here too, and this runner is where the rule was MISSING.
|
|
244
|
+
*
|
|
245
|
+
* The other two runners check it (through `ambiguityDrift`); this one took `refs[0]`, dispatched and
|
|
246
|
+
* returned `ok: true`. `by: QueryBy.COMPONENT` matches by component NAME, and a component rendered
|
|
247
|
+
* once per row — a table's action button, a card's menu — matches as many times as there are rows.
|
|
248
|
+
* So the guessed-green shape is not an edge case for this anchor kind, it is its ordinary one, and
|
|
249
|
+
* it is the exact defect #1227 describes: a click on the wrong element reported as a pass because
|
|
250
|
+
* the verdict reads `drift` and `ok` and never reads which element was hit.
|
|
251
|
+
*/
|
|
252
|
+
const ambiguous = ambiguityDrift(anchor, refs);
|
|
253
|
+
if (ambiguous !== null) {
|
|
254
|
+
return { step: index, tool: step.tool, anchor: label, ok: false, drift: ambiguous };
|
|
255
|
+
}
|
|
198
256
|
const ref = refs[0] ?? '';
|
|
199
257
|
// Shared with the role path so both get the same action window AND the same stale-ref retry —
|
|
200
258
|
// this used to dispatch inline, which is how one anchor kind could gain a fix the other lacked.
|
|
201
|
-
return await actOnResolvedRef(session, step, index, label, ref, confirmDangerous, anchorFieldName(anchor),
|
|
259
|
+
return await actOnResolvedRef(session, step, index, label, ref, confirmDangerous, anchorFieldName(anchor),
|
|
260
|
+
// Same rule one layer deeper, same helper: a re-render that turns one match into three must not
|
|
261
|
+
// be dispatched at either, and it must say so as a drift rather than as a dead ref. This callback
|
|
262
|
+
// used to return `refs[0]` unconditionally.
|
|
263
|
+
async () => reresolved(anchor, (await resolveQuery(session, componentQueryArgs(anchor), sleep)).refs));
|
|
202
264
|
}
|
|
203
265
|
/**
|
|
204
266
|
* DEGRADED_ANCHOR_ROLE is a MARKER ("no anchor could be determined"), never a locator.
|
|
@@ -268,6 +330,63 @@ export function anchorPrecondition(anchor) {
|
|
|
268
330
|
}
|
|
269
331
|
return undefined;
|
|
270
332
|
}
|
|
333
|
+
/** What is added to a stale-ref error when it cannot be traced to one sub-step of the sequence. */
|
|
334
|
+
const STALE_SUB_STEP_UNKNOWN = ' (which sub-step that was could not be told, so the ones before it may have run)';
|
|
335
|
+
/** Where a ref that stayed stale sat in the sequence, and that every sub-step before it ran. */
|
|
336
|
+
const staleSubStepPosition = (at, total) => ` (sub-step ${String(at)} of ${String(total)}; ${String(at)} before it ran)`;
|
|
337
|
+
/**
|
|
338
|
+
* Why a step whose target kept going stale is unverifiable, as the replay result tells a reader.
|
|
339
|
+
* `step` is the index of the step that could not be acted on.
|
|
340
|
+
*/
|
|
341
|
+
const staleTargetReason = (step) => `step ${String(step)} could not be acted on: its target kept going stale, because the ` +
|
|
342
|
+
`page re-rendered between finding the element and using it, and the replay looked it up ` +
|
|
343
|
+
`again for as long as that got it further. The steps before it are reported and nothing ` +
|
|
344
|
+
`after it ran. Nothing here says the app failed. If an earlier step makes the page ` +
|
|
345
|
+
`re-render, record that step's consequence with \`expect\` so the replay waits for the new ` +
|
|
346
|
+
`render before it moves on.`;
|
|
347
|
+
/**
|
|
348
|
+
* The sub-step, among live[from, to), whose ref the page said it could not find.
|
|
349
|
+
*
|
|
350
|
+
* The page echoes the ref in "ref 'x' no longer resolves to an element". A ref it shortened, or one
|
|
351
|
+
* that is not among these, gives no answer. Neither does a ref that two sub-steps share: the first of
|
|
352
|
+
* them may already have run, and may be what re-rendered the page, which makes the later one the stale
|
|
353
|
+
* one, and nothing in the message says so. The caller must not guess which actions already ran.
|
|
354
|
+
*/
|
|
355
|
+
function staleSubStep(error, live, from, to) {
|
|
356
|
+
const named = /ref '([^']*)'/.exec(error)?.[1];
|
|
357
|
+
if (named === undefined)
|
|
358
|
+
return undefined;
|
|
359
|
+
const sharing = live.flatMap((one, at) => at >= from && at < to && one.ref === named ? [at] : []);
|
|
360
|
+
const [only] = sharing;
|
|
361
|
+
return 1 === sharing.length ? only : undefined;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The honest answer when a step's target kept going stale.
|
|
365
|
+
*
|
|
366
|
+
* Replay looks the element up again and goes on while that gets it further, so a step still stale
|
|
367
|
+
* after that is one the page re-rendered around faster than it could be targeted. Reporting it as
|
|
368
|
+
* `error` with the browser's own sentence says "your app broke", which is a claim nothing observed
|
|
369
|
+
* and sends a reader into product code that is fine. It is the same bucket as a document that went
|
|
370
|
+
* away: the steps before it are real and are attached, the step that could not be acted on is among
|
|
371
|
+
* them with its index, and nothing after it ran.
|
|
372
|
+
*
|
|
373
|
+
* Left alone, so the ordinary verdict stands, when anything else is in the run: a drifted anchor is
|
|
374
|
+
* drift whatever else happened, and a failure that is not staleness is a failure.
|
|
375
|
+
*/
|
|
376
|
+
export function staleTargetResult(name, steps) {
|
|
377
|
+
if (steps.some((step) => step.drift !== undefined))
|
|
378
|
+
return undefined;
|
|
379
|
+
const failed = steps.filter((step) => !step.ok);
|
|
380
|
+
const [only] = failed;
|
|
381
|
+
if (failed.length !== 1 || only === undefined || !isStaleRefError(only.error))
|
|
382
|
+
return undefined;
|
|
383
|
+
return {
|
|
384
|
+
name,
|
|
385
|
+
status: ReplayStatus.UNVERIFIABLE,
|
|
386
|
+
steps: [...steps],
|
|
387
|
+
unverifiable: { reason: staleTargetReason(only.step) },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
271
390
|
/**
|
|
272
391
|
* Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
|
|
273
392
|
* ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
|
|
@@ -281,37 +400,62 @@ export function anchorPrecondition(anchor) {
|
|
|
281
400
|
export async function runSequenceStep(session, step, index, subs, confirmDangerous, sleep,
|
|
282
401
|
/** Testids whose presence is deliberately NOT asserted — the LLM-output case. */
|
|
283
402
|
dynamic = new Set()) {
|
|
284
|
-
|
|
285
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Find one sub-step's element as the page is NOW. Either the live step to dispatch, or the step
|
|
405
|
+
* result that ends the run: a degraded anchor, or one that is no longer on the page.
|
|
406
|
+
*
|
|
407
|
+
* `afterRerender` is the second look, taken because the page re-rendered under a sub-step. An anchor
|
|
408
|
+
* that now matches several elements is drift there, as it is for a single step's retry: the first of
|
|
409
|
+
* the new matches may be an element the recording did not mean, and acting on it would report a pass
|
|
410
|
+
* for a click nobody can attribute. The first look keeps taking the first match.
|
|
411
|
+
*/
|
|
412
|
+
const resolveSub = async (sub, subIndex, afterRerender = false) => {
|
|
286
413
|
const label = `${anchorLabel(sub.anchor)} (sub-step ${String(subIndex)})`;
|
|
287
414
|
const queryArgs = anchorQueryArgs(sub.anchor);
|
|
288
415
|
if (null === queryArgs)
|
|
289
|
-
return degradedStepResult(step, index, label);
|
|
416
|
+
return { stop: degradedStepResult(step, index, label) };
|
|
290
417
|
const { refs, hint } = await resolveQuery(session, queryArgs, sleep);
|
|
291
|
-
const
|
|
418
|
+
const ambiguous = afterRerender ? ambiguityDrift(sub.anchor, refs) : null;
|
|
419
|
+
if (ambiguous !== null) {
|
|
420
|
+
return { stop: { step: index, tool: step.tool, anchor: label, ok: false, drift: ambiguous } };
|
|
421
|
+
}
|
|
422
|
+
const ref = afterRerender ? refFor(sub.anchor, refs) : refs[0];
|
|
292
423
|
if (ref === undefined) {
|
|
293
424
|
return {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
425
|
+
stop: {
|
|
426
|
+
step: index,
|
|
427
|
+
tool: step.tool,
|
|
428
|
+
anchor: label,
|
|
429
|
+
ok: false,
|
|
430
|
+
drift: sub.anchor.kind === AnchorKind.TESTID
|
|
431
|
+
? testidDrift(sub.anchor.value, hint)
|
|
432
|
+
: {
|
|
433
|
+
reasonKind: DriftReason.COMPONENT_NOT_FOUND,
|
|
434
|
+
reason: `anchor ${label} not found`,
|
|
435
|
+
anchor: label,
|
|
436
|
+
nearest: null,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
306
439
|
};
|
|
307
440
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
441
|
+
return {
|
|
442
|
+
live: {
|
|
443
|
+
ref,
|
|
444
|
+
action: sub.action ?? '',
|
|
445
|
+
// Each sub-step carries its OWN anchor, so each gets its own field name. A sequence that ends
|
|
446
|
+
// in a login is the shape this was reported on, and the sub-step is where the fill lives.
|
|
447
|
+
args: replayActionArgs(sub.args, confirmDangerous, anchorFieldName(sub.anchor)),
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
// Every anchor is checked before anything runs, so a sub-step that has drifted away is reported as
|
|
452
|
+
// drift and not discovered half way through a sequence that has already acted.
|
|
453
|
+
const live = [];
|
|
454
|
+
for (const [subIndex, sub] of subs.entries()) {
|
|
455
|
+
const resolved = await resolveSub(sub, subIndex);
|
|
456
|
+
if ('stop' in resolved)
|
|
457
|
+
return resolved.stop;
|
|
458
|
+
live.push(resolved.live);
|
|
315
459
|
}
|
|
316
460
|
const result = {
|
|
317
461
|
step: index,
|
|
@@ -320,20 +464,71 @@ dynamic = new Set()) {
|
|
|
320
464
|
ok: true,
|
|
321
465
|
};
|
|
322
466
|
/** One dispatch through the SAME command either way, so batched and walked replay identically. */
|
|
323
|
-
const
|
|
467
|
+
const send = async (steps) => {
|
|
324
468
|
session.beginAction?.(ReticleTool.FLOW_REPLAY, { steps: steps.length });
|
|
325
|
-
let act;
|
|
326
469
|
try {
|
|
327
|
-
|
|
470
|
+
return await session.command(ReticleCommand.ACT_SEQUENCE, { steps });
|
|
328
471
|
}
|
|
329
472
|
finally {
|
|
330
473
|
session.finishAction?.();
|
|
331
474
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
475
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* Run sub-steps [from, to), and survive the page re-rendering under them.
|
|
478
|
+
*
|
|
479
|
+
* The refs were resolved before anything ran, and the first action of a sequence is allowed to
|
|
480
|
+
* re-render the page, so by the time a later sub-step is reached its ref may be dead. The same
|
|
481
|
+
* anchor is almost always still there under a new ref; what was missing is a second look.
|
|
482
|
+
*
|
|
483
|
+
* The page names the ref it could not find, which says WHICH sub-step failed and so which ones
|
|
484
|
+
* already ran, as long as no other sub-step holds the same ref. Only the rest is resolved again and
|
|
485
|
+
* sent, so nothing acts twice: starting over is what turns one click into two. A stale ref is the
|
|
486
|
+
* only failure retried, and the retry goes on only while it gets further. A sub-step that is stale
|
|
487
|
+
* again at the same point is a target that does not hold still, and the run says so, with how far
|
|
488
|
+
* it got.
|
|
489
|
+
*/
|
|
490
|
+
const dispatchFrom = async (from, to) => {
|
|
491
|
+
let start = from;
|
|
492
|
+
let staleBefore = -1;
|
|
493
|
+
for (;;) {
|
|
494
|
+
const act = await send(live.slice(start, to));
|
|
495
|
+
if (act.ok)
|
|
496
|
+
return true;
|
|
497
|
+
const error = act.error ?? 'command failed';
|
|
498
|
+
if (!isStaleRefError(error)) {
|
|
499
|
+
result.ok = false;
|
|
500
|
+
result.error = replayDestructiveActionHint(error);
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
const staleAt = staleSubStep(error, live, start, to);
|
|
504
|
+
if (staleAt === undefined) {
|
|
505
|
+
result.ok = false;
|
|
506
|
+
result.error = error + STALE_SUB_STEP_UNKNOWN;
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
if (staleAt <= staleBefore) {
|
|
510
|
+
result.ok = false;
|
|
511
|
+
result.error = error + staleSubStepPosition(staleAt, subs.length);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
staleBefore = staleAt;
|
|
515
|
+
await waitForReaction(session, 0, STALE_REF_GRACE_MS, { sleep });
|
|
516
|
+
for (let at = staleAt; at < to; at += 1) {
|
|
517
|
+
const sub = subs[at];
|
|
518
|
+
if (sub === undefined)
|
|
519
|
+
continue;
|
|
520
|
+
const resolved = await resolveSub(sub, at, true);
|
|
521
|
+
if ('stop' in resolved) {
|
|
522
|
+
result.ok = false;
|
|
523
|
+
result.anchor = resolved.stop.anchor;
|
|
524
|
+
if (resolved.stop.drift !== undefined)
|
|
525
|
+
result.drift = resolved.stop.drift;
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
live[at] = resolved.live;
|
|
529
|
+
}
|
|
530
|
+
start = staleAt;
|
|
531
|
+
}
|
|
337
532
|
};
|
|
338
533
|
/** The sub-step's own expectation, unless its testid is deliberately unasserted. */
|
|
339
534
|
const declaredBy = (sub) => {
|
|
@@ -368,14 +563,13 @@ dynamic = new Set()) {
|
|
|
368
563
|
*/
|
|
369
564
|
const declaring = subs.some((sub) => declaredBy(sub) !== undefined);
|
|
370
565
|
if (!declaring) {
|
|
371
|
-
await
|
|
566
|
+
await dispatchFrom(0, live.length);
|
|
372
567
|
return result;
|
|
373
568
|
}
|
|
374
569
|
for (const [subIndex, sub] of subs.entries()) {
|
|
375
|
-
|
|
376
|
-
if (one === undefined)
|
|
570
|
+
if (live[subIndex] === undefined)
|
|
377
571
|
continue;
|
|
378
|
-
if (!(await
|
|
572
|
+
if (!(await dispatchFrom(subIndex, subIndex + 1)))
|
|
379
573
|
return result;
|
|
380
574
|
const testid = declaredBy(sub);
|
|
381
575
|
if (testid !== undefined && !(await assertDeclared(testid)))
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
* returned unchanged.
|
|
25
25
|
*/
|
|
26
26
|
import { FlowStore } from './flows.js';
|
|
27
|
-
import type {
|
|
27
|
+
import type { ProjectTarget } from '../../memory/project/session-root.js';
|
|
28
28
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
29
|
-
export declare function flowsForSession(deps: ToolDeps,
|
|
29
|
+
export declare function flowsForSession(deps: ToolDeps, target: ProjectTarget): {
|
|
30
30
|
flows: FlowStore;
|
|
31
31
|
root: string;
|
|
32
32
|
};
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* returned unchanged.
|
|
25
25
|
*/
|
|
26
26
|
import { FlowStore } from './flows.js';
|
|
27
|
-
export function flowsForSession(deps,
|
|
28
|
-
const resolved = deps.artifactRootFor?.(projectId);
|
|
27
|
+
export function flowsForSession(deps, target) {
|
|
28
|
+
const resolved = deps.artifactRootFor?.(target.projectId, target.origin);
|
|
29
29
|
if (resolved === undefined || resolved.root === deps.reticleRoot) {
|
|
30
30
|
return { flows: deps.flows, root: deps.reticleRoot };
|
|
31
31
|
}
|