@veraxhq/verax 0.1.0 → 0.2.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/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FLOW DETECTION MODULE
|
|
3
|
+
*
|
|
4
|
+
* Detects multi-step flow failures - when a sequence of interactions
|
|
5
|
+
* in a flow should work together but one step fails silently.
|
|
6
|
+
*
|
|
7
|
+
* FLOW INTELLIGENCE v1:
|
|
8
|
+
* - Groups traces by flowId
|
|
9
|
+
* - Validates each flow has 2+ proven expectations
|
|
10
|
+
* - Detects silent failures in flow steps
|
|
11
|
+
* - Checks for recovery in subsequent steps
|
|
12
|
+
* - Emits flow_silent_failure findings when appropriate
|
|
13
|
+
* - Suppresses per-step findings when flow finding is emitted
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getUrlPath } from './evidence-validator.js';
|
|
17
|
+
import {
|
|
18
|
+
hasMeaningfulUrlChange,
|
|
19
|
+
hasVisibleChange,
|
|
20
|
+
hasDomChange
|
|
21
|
+
} from './comparison.js';
|
|
22
|
+
import {
|
|
23
|
+
expectsNavigation,
|
|
24
|
+
matchExpectation
|
|
25
|
+
} from './expectation-model.js';
|
|
26
|
+
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
27
|
+
import { computeConfidence } from './confidence-engine.js';
|
|
28
|
+
import { enrichFindingWithExplanations } from './finding-detector.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect flow-level silent failures in multi-step flows.
|
|
32
|
+
*
|
|
33
|
+
* @param {Array} traces - Observation traces from observe phase
|
|
34
|
+
* @param {Object} manifest - Expectation manifest with staticExpectations
|
|
35
|
+
* @param {Array} findings - Array of findings (will be mutated to suppress per-step findings)
|
|
36
|
+
* @param {Array} coverageGaps - Coverage gaps data
|
|
37
|
+
* @param {Object} helpers - Helper functions { enrichFindingWithDecisionSignals, computeConfidence }
|
|
38
|
+
* @returns {Array} Array of flow_silent_failure findings
|
|
39
|
+
*/
|
|
40
|
+
export function detectFlowSilentFailures(traces, manifest, findings, coverageGaps, helpers = {}) {
|
|
41
|
+
const flowFindings = [];
|
|
42
|
+
const projectDir = manifest.projectDir;
|
|
43
|
+
const { enrichFindingWithDecisionSignals } = helpers;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute confidence for a flow finding.
|
|
47
|
+
*/
|
|
48
|
+
function computeFlowFindingConfidence(flowFinding, matchedExpectation, trace, comparisons) {
|
|
49
|
+
const sensors = trace.sensors || {};
|
|
50
|
+
const findingType = 'flow_silent_failure';
|
|
51
|
+
|
|
52
|
+
const confidence = computeConfidence({
|
|
53
|
+
findingType,
|
|
54
|
+
expectation: matchedExpectation || {},
|
|
55
|
+
sensors: {
|
|
56
|
+
network: sensors.network || {},
|
|
57
|
+
console: sensors.console || {},
|
|
58
|
+
uiSignals: sensors.uiSignals || {}
|
|
59
|
+
},
|
|
60
|
+
comparisons: {
|
|
61
|
+
hasUrlChange: comparisons.hasUrlChange || false,
|
|
62
|
+
hasDomChange: comparisons.hasDomChange || false,
|
|
63
|
+
hasVisibleChange: comparisons.hasVisibleChange || false
|
|
64
|
+
},
|
|
65
|
+
attemptMeta: {}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return confidence;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Group traces by flowId
|
|
72
|
+
const flowsByFlowId = {};
|
|
73
|
+
let tracesWithFlowId = 0;
|
|
74
|
+
for (const trace of traces) {
|
|
75
|
+
const flowId = trace.flow?.flowId;
|
|
76
|
+
if (!flowId) continue; // Flow tracking not available for this trace
|
|
77
|
+
tracesWithFlowId++;
|
|
78
|
+
|
|
79
|
+
if (!flowsByFlowId[flowId]) {
|
|
80
|
+
flowsByFlowId[flowId] = [];
|
|
81
|
+
}
|
|
82
|
+
flowsByFlowId[flowId].push(trace);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// FLOW INTELLIGENCE v1: Only process if we have traces with flowIds
|
|
86
|
+
if (tracesWithFlowId === 0) {
|
|
87
|
+
return flowFindings; // No flow tracking available
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Detect flow silent failures
|
|
91
|
+
for (const flowId in flowsByFlowId) {
|
|
92
|
+
const flowTraces = flowsByFlowId[flowId];
|
|
93
|
+
|
|
94
|
+
// Flow must have at least 2 steps
|
|
95
|
+
if (flowTraces.length < 2) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// FLOW INTELLIGENCE v1: Check if flow has PROVEN expectations (at least 2 steps with PROVEN expectations)
|
|
100
|
+
let stepCountWithProvenExpectation = 0;
|
|
101
|
+
for (const trace of flowTraces) {
|
|
102
|
+
const interaction = trace.interaction;
|
|
103
|
+
const beforeUrl = trace.before.url;
|
|
104
|
+
|
|
105
|
+
// Check for any PROVEN expectation (navigation, network, state)
|
|
106
|
+
let hasProvenExpectation = false;
|
|
107
|
+
|
|
108
|
+
// Check navigation
|
|
109
|
+
if (expectsNavigation(manifest, interaction, beforeUrl)) {
|
|
110
|
+
hasProvenExpectation = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check network
|
|
114
|
+
if (!hasProvenExpectation && manifest.staticExpectations) {
|
|
115
|
+
const networkExp = manifest.staticExpectations.find(e =>
|
|
116
|
+
e.type === 'network_action' &&
|
|
117
|
+
isProvenExpectation(e) &&
|
|
118
|
+
matchExpectation(e, interaction, beforeUrl)
|
|
119
|
+
);
|
|
120
|
+
if (networkExp) {
|
|
121
|
+
hasProvenExpectation = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasProvenExpectation) {
|
|
126
|
+
stepCountWithProvenExpectation++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Only process flows with 2+ PROVEN steps
|
|
131
|
+
if (stepCountWithProvenExpectation < 2) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Look for silent failures followed by lack of recovery
|
|
136
|
+
const _hasSilentFailure = false;
|
|
137
|
+
let failedStepIndex = -1;
|
|
138
|
+
const _failedExpectation = null;
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < flowTraces.length; i++) {
|
|
141
|
+
const trace = flowTraces[i];
|
|
142
|
+
const interaction = trace.interaction;
|
|
143
|
+
const beforeUrl = trace.before.url;
|
|
144
|
+
const afterUrl = trace.after.url;
|
|
145
|
+
const beforeScreenshot = trace.before.screenshot;
|
|
146
|
+
const afterScreenshot = trace.after.screenshot;
|
|
147
|
+
|
|
148
|
+
// Check for PROVEN expectations at this step
|
|
149
|
+
let matchedExpectation = null;
|
|
150
|
+
let expectationType = null;
|
|
151
|
+
let isSilentFailure = false;
|
|
152
|
+
|
|
153
|
+
// Check navigation expectation
|
|
154
|
+
if (expectsNavigation(manifest, interaction, beforeUrl)) {
|
|
155
|
+
const navExp = manifest.staticExpectations?.find(e =>
|
|
156
|
+
e.type === 'navigation' &&
|
|
157
|
+
isProvenExpectation(e) &&
|
|
158
|
+
e.fromPath && getUrlPath(beforeUrl) &&
|
|
159
|
+
e.fromPath.replace(/\/$/, '') === getUrlPath(beforeUrl).replace(/\/$/, '') &&
|
|
160
|
+
matchExpectation(e, interaction, beforeUrl)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (navExp) {
|
|
164
|
+
matchedExpectation = navExp;
|
|
165
|
+
expectationType = 'navigation';
|
|
166
|
+
|
|
167
|
+
const afterUrlPath = getUrlPath(afterUrl) || '/';
|
|
168
|
+
// Normalize paths: remove .html extension and trailing slashes for comparison
|
|
169
|
+
const normalizePath = (path) => {
|
|
170
|
+
if (!path) return '/';
|
|
171
|
+
let normalized = path.toLowerCase().trim().replace(/\/$/, '') || '/';
|
|
172
|
+
// Remove .html extension for comparison
|
|
173
|
+
normalized = normalized.replace(/\.html$/, '');
|
|
174
|
+
return normalized;
|
|
175
|
+
};
|
|
176
|
+
const normalizedAfter = normalizePath(afterUrlPath);
|
|
177
|
+
const normalizedTarget = normalizePath(navExp.targetPath || '');
|
|
178
|
+
|
|
179
|
+
const urlMatchesTarget = normalizedAfter === normalizedTarget;
|
|
180
|
+
const hasVisibleChangeResult = beforeScreenshot && afterScreenshot ?
|
|
181
|
+
hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir) : false;
|
|
182
|
+
const hasDomChangeResult = hasDomChange(trace);
|
|
183
|
+
|
|
184
|
+
const hasEffect = urlMatchesTarget || hasVisibleChangeResult || hasDomChangeResult;
|
|
185
|
+
const uiSignals = trace.sensors?.uiSignals || {};
|
|
186
|
+
const uiAfter = uiSignals.after || {};
|
|
187
|
+
const hasUIFeedback = uiAfter.uiFeedbackDetected === true || uiAfter.changed === true;
|
|
188
|
+
|
|
189
|
+
// Navigation silent failure: expected navigation didn't occur and no UI feedback
|
|
190
|
+
// BUT: in flow context, if navigation fails but we have subsequent steps, don't mark as silent failure yet
|
|
191
|
+
// Let flow detection handle it
|
|
192
|
+
if (!hasEffect && !hasUIFeedback) {
|
|
193
|
+
isSilentFailure = true;
|
|
194
|
+
} else {
|
|
195
|
+
// Navigation succeeded - clear any silent failure flag
|
|
196
|
+
isSilentFailure = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check network expectation (check even if navigation was found, as different steps may have different types)
|
|
202
|
+
if (!matchedExpectation && manifest.staticExpectations) {
|
|
203
|
+
const networkExp = manifest.staticExpectations.find(e =>
|
|
204
|
+
e.type === 'network_action' &&
|
|
205
|
+
isProvenExpectation(e) &&
|
|
206
|
+
matchExpectation(e, interaction, beforeUrl)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (networkExp) {
|
|
210
|
+
matchedExpectation = networkExp;
|
|
211
|
+
expectationType = 'network_action';
|
|
212
|
+
|
|
213
|
+
const networkData = trace.sensors?.network || { totalRequests: 0, failedRequests: 0 };
|
|
214
|
+
const uiSignals = trace.sensors?.uiSignals || {};
|
|
215
|
+
const uiAfter = uiSignals.after || {};
|
|
216
|
+
const hasUIFeedback = uiAfter.uiFeedbackDetected === true || uiAfter.changed === true;
|
|
217
|
+
|
|
218
|
+
// Silent failure: network request failed OR missing with no UI feedback
|
|
219
|
+
if ((networkData.failedRequests > 0 || networkData.totalRequests === 0) && !hasUIFeedback) {
|
|
220
|
+
isSilentFailure = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Also check network expectation even if navigation was matched (for step 2 of flow)
|
|
226
|
+
if (matchedExpectation && matchedExpectation.type === 'navigation' && manifest.staticExpectations) {
|
|
227
|
+
const networkExp = manifest.staticExpectations.find(e =>
|
|
228
|
+
e.type === 'network_action' &&
|
|
229
|
+
isProvenExpectation(e) &&
|
|
230
|
+
matchExpectation(e, interaction, beforeUrl)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (networkExp) {
|
|
234
|
+
// This step has both navigation and network expectations - check network failure
|
|
235
|
+
const networkData = trace.sensors?.network || { totalRequests: 0, failedRequests: 0 };
|
|
236
|
+
const uiSignals = trace.sensors?.uiSignals || {};
|
|
237
|
+
const uiAfter = uiSignals.after || {};
|
|
238
|
+
const hasUIFeedback = uiAfter.uiFeedbackDetected === true || uiAfter.changed === true;
|
|
239
|
+
|
|
240
|
+
// If network fails silently, this is the failure we want to detect
|
|
241
|
+
if ((networkData.failedRequests > 0 || networkData.totalRequests === 0) && !hasUIFeedback) {
|
|
242
|
+
matchedExpectation = networkExp;
|
|
243
|
+
expectationType = 'network_action';
|
|
244
|
+
isSilentFailure = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (isSilentFailure && matchedExpectation) {
|
|
250
|
+
// Silent failure detected at this step
|
|
251
|
+
const _hasSilentFailure = true;
|
|
252
|
+
failedStepIndex = i;
|
|
253
|
+
const _failedExpectation = matchedExpectation;
|
|
254
|
+
|
|
255
|
+
// Check if subsequent steps show UI recovery
|
|
256
|
+
let hasSubsequentRecovery = false;
|
|
257
|
+
if (i + 1 < flowTraces.length) {
|
|
258
|
+
const nextTrace = flowTraces[i + 1];
|
|
259
|
+
const nextBeforeScreenshot = nextTrace.before.screenshot;
|
|
260
|
+
const nextAfterScreenshot = nextTrace.after.screenshot;
|
|
261
|
+
const nextUISignals = nextTrace.sensors?.uiSignals || {};
|
|
262
|
+
const nextUIAfter = nextUISignals.after || {};
|
|
263
|
+
|
|
264
|
+
if (nextBeforeScreenshot && nextAfterScreenshot) {
|
|
265
|
+
hasSubsequentRecovery = hasVisibleChange(nextBeforeScreenshot, nextAfterScreenshot, projectDir) ||
|
|
266
|
+
nextUIAfter.uiFeedbackDetected === true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Emit flow_silent_failure only if no recovery in next step
|
|
271
|
+
if (!hasSubsequentRecovery) {
|
|
272
|
+
const hasUrlChange = hasMeaningfulUrlChange(beforeUrl, afterUrl);
|
|
273
|
+
const hasVisibleChangeResult = beforeScreenshot && afterScreenshot ?
|
|
274
|
+
hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir) : false;
|
|
275
|
+
const hasDomChangeResult = hasDomChange(trace);
|
|
276
|
+
|
|
277
|
+
// Build prior steps summary
|
|
278
|
+
const priorSteps = [];
|
|
279
|
+
for (let j = 0; j < i; j++) {
|
|
280
|
+
priorSteps.push({
|
|
281
|
+
stepIndex: j,
|
|
282
|
+
interaction: flowTraces[j].interaction,
|
|
283
|
+
url: flowTraces[j].after.url
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const flowFinding = {
|
|
288
|
+
type: 'flow_silent_failure',
|
|
289
|
+
flowId: flowId,
|
|
290
|
+
failedStepIndex: failedStepIndex,
|
|
291
|
+
priorStepsCount: failedStepIndex,
|
|
292
|
+
priorSteps: priorSteps,
|
|
293
|
+
interaction: {
|
|
294
|
+
type: interaction.type,
|
|
295
|
+
selector: interaction.selector,
|
|
296
|
+
label: interaction.label
|
|
297
|
+
},
|
|
298
|
+
reason: 'Silent failure in multi-step flow with no recovery in subsequent step',
|
|
299
|
+
evidence: {
|
|
300
|
+
before: beforeScreenshot,
|
|
301
|
+
after: afterScreenshot,
|
|
302
|
+
beforeUrl: beforeUrl,
|
|
303
|
+
afterUrl: afterUrl,
|
|
304
|
+
failedExpectation: {
|
|
305
|
+
type: expectationType,
|
|
306
|
+
sourceRef: matchedExpectation.sourceRef,
|
|
307
|
+
handlerRef: matchedExpectation.handlerRef
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
flowFinding.confidence = computeFlowFindingConfidence(
|
|
313
|
+
flowFinding,
|
|
314
|
+
matchedExpectation,
|
|
315
|
+
trace,
|
|
316
|
+
{ hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult }
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
enrichFindingWithExplanations(flowFinding, trace);
|
|
320
|
+
if (enrichFindingWithDecisionSignals) {
|
|
321
|
+
enrichFindingWithDecisionSignals(flowFinding, trace);
|
|
322
|
+
}
|
|
323
|
+
flowFindings.push(flowFinding);
|
|
324
|
+
|
|
325
|
+
// Suppress per-step findings when flow finding is emitted
|
|
326
|
+
// Remove any findings for this step AND any previous steps in the flow from the main findings array
|
|
327
|
+
// Match by interaction details and also by finding type (network_silent_failure, navigation_silent_failure, etc.)
|
|
328
|
+
const findingTypesToSuppress = ['network_silent_failure', 'navigation_silent_failure', 'missing_network_action', 'no_effect_silent_failure', 'silent_failure'];
|
|
329
|
+
|
|
330
|
+
// Suppress findings for ALL steps in this flow (not just the failed step)
|
|
331
|
+
for (let stepIdx = 0; stepIdx <= i; stepIdx++) {
|
|
332
|
+
const stepTrace = flowTraces[stepIdx];
|
|
333
|
+
const stepInteraction = stepTrace.interaction;
|
|
334
|
+
const stepBeforeUrl = stepTrace.before.url;
|
|
335
|
+
const stepAfterUrl = stepTrace.after.url;
|
|
336
|
+
|
|
337
|
+
for (let k = findings.length - 1; k >= 0; k--) {
|
|
338
|
+
const finding = findings[k];
|
|
339
|
+
if (finding.interaction &&
|
|
340
|
+
findingTypesToSuppress.includes(finding.type)) {
|
|
341
|
+
// Match by selector, label, or by URL context for this step
|
|
342
|
+
const matchesSelector = finding.interaction.selector === stepInteraction.selector;
|
|
343
|
+
const matchesLabel = finding.interaction.label === stepInteraction.label;
|
|
344
|
+
const matchesUrl = (finding.evidence?.beforeUrl === stepBeforeUrl) ||
|
|
345
|
+
(finding.evidence?.afterUrl === stepAfterUrl) ||
|
|
346
|
+
(finding.evidence?.beforeUrl === stepTrace.before.url) ||
|
|
347
|
+
(finding.evidence?.afterUrl === stepTrace.after.url);
|
|
348
|
+
|
|
349
|
+
if (matchesSelector || matchesLabel || matchesUrl) {
|
|
350
|
+
findings.splice(k, 1);
|
|
351
|
+
// Note: findingsFromProven is in outer scope, but we can't access it here
|
|
352
|
+
// The caller will handle the adjustment
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Only report first silent failure in flow - break after emitting finding
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return flowFindings;
|
|
366
|
+
}
|