@veraxhq/verax 0.1.0 → 0.2.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 +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -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 +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -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 +101 -0
- package/src/verax/cli/wizard.js +98 -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 +403 -0
- package/src/verax/core/incremental-store.js +237 -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 +521 -0
- package/src/verax/detect/comparison.js +2 -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 +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- 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 +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- 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 +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- 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 +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -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 +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -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 +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -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 +14 -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 +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import {
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { expectsNavigation } from './expectation-model.js';
|
|
3
4
|
import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from './comparison.js';
|
|
4
5
|
import { writeFindings } from './findings-writer.js';
|
|
5
6
|
import { getUrlPath } from './evidence-validator.js';
|
|
6
7
|
import { classifySkipReason, collectSkipReasons } from './skip-classifier.js';
|
|
7
|
-
import {
|
|
8
|
-
import { computeConfidence } from './confidence-engine.js';
|
|
8
|
+
import { detectInteractiveFindings } from './interactive-findings.js';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
function createFindingWithConfidence(baseFinding, expectationInfo, sensors, comparisons) {
|
|
12
|
-
const confidence = computeConfidence({
|
|
13
|
-
findingType: baseFinding.type,
|
|
14
|
-
expectation: expectationInfo,
|
|
15
|
-
sensors: sensors,
|
|
16
|
-
comparisons: comparisons,
|
|
17
|
-
attemptMeta: {}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
...baseFinding,
|
|
22
|
-
confidence
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function detect(manifestPath, tracesPath, validation = null, artifactPaths = null) {
|
|
10
|
+
export async function detect(manifestPath, tracesPath, validation = null) {
|
|
27
11
|
if (!existsSync(manifestPath)) {
|
|
28
12
|
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
29
13
|
}
|
|
@@ -52,334 +36,236 @@ export async function detect(manifestPath, tracesPath, validation = null, artifa
|
|
|
52
36
|
const beforeScreenshot = trace.before.screenshot;
|
|
53
37
|
const afterScreenshot = trace.after.screenshot;
|
|
54
38
|
|
|
55
|
-
|
|
56
|
-
const attemptMeta = trace.meta || {};
|
|
57
|
-
const expectationInfo = getExpectation(manifest, interaction, beforeUrl, attemptMeta);
|
|
39
|
+
const expectsNav = expectsNavigation(manifest, interaction, beforeUrl);
|
|
58
40
|
|
|
59
|
-
|
|
60
|
-
if (!expectationInfo.hasExpectation || expectationInfo.proof !== ExpectationProof.PROVEN_EXPECTATION) {
|
|
41
|
+
if (!expectsNav) {
|
|
61
42
|
interactionsSkippedNoExpectation++;
|
|
62
|
-
const skipReason =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
43
|
+
const skipReason = classifySkipReason(manifest, interaction, beforeUrl, validation);
|
|
44
|
+
if (skipReason) {
|
|
45
|
+
skips.push({
|
|
46
|
+
code: skipReason.code,
|
|
47
|
+
message: skipReason.message,
|
|
48
|
+
interaction: {
|
|
49
|
+
type: interaction.type,
|
|
50
|
+
selector: interaction.selector,
|
|
51
|
+
label: interaction.label
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
73
55
|
continue;
|
|
74
56
|
}
|
|
75
57
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
interactionsAnalyzed++;
|
|
58
|
+
let expectedTargetPath = null;
|
|
59
|
+
let expectationType = null;
|
|
60
|
+
let selectorMismatch = false;
|
|
61
|
+
let multipleMatches = false;
|
|
81
62
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
63
|
+
if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
|
|
64
|
+
const beforePath = getUrlPath(beforeUrl);
|
|
65
|
+
if (beforePath) {
|
|
66
|
+
const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
|
|
67
|
+
const matchingExpectations = [];
|
|
68
|
+
|
|
69
|
+
for (const expectation of manifest.staticExpectations) {
|
|
70
|
+
const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
|
|
71
|
+
if (normalizedFrom === normalizedBefore) {
|
|
72
|
+
const selectorHint = expectation.selectorHint || '';
|
|
73
|
+
const interactionSelector = interaction.selector || '';
|
|
74
|
+
|
|
75
|
+
if (selectorHint && interactionSelector) {
|
|
76
|
+
const normalizedSelectorHint = selectorHint.replace(/[\[\]()]/g, '');
|
|
77
|
+
const normalizedInteractionSelector = interactionSelector.replace(/[\[\]()]/g, '');
|
|
78
|
+
|
|
79
|
+
if (selectorHint === interactionSelector ||
|
|
80
|
+
selectorHint.includes(interactionSelector) ||
|
|
81
|
+
interactionSelector.includes(normalizedSelectorHint) ||
|
|
82
|
+
normalizedSelectorHint === normalizedInteractionSelector) {
|
|
83
|
+
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
84
|
+
matchingExpectations.push(expectation);
|
|
85
|
+
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
86
|
+
matchingExpectations.push(expectation);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
90
|
+
selectorMismatch = true;
|
|
91
|
+
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
92
|
+
selectorMismatch = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else if (!selectorHint && !interactionSelector) {
|
|
96
|
+
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
97
|
+
matchingExpectations.push(expectation);
|
|
98
|
+
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
99
|
+
matchingExpectations.push(expectation);
|
|
100
|
+
}
|
|
118
101
|
}
|
|
119
|
-
}
|
|
120
|
-
findings.push(finding);
|
|
102
|
+
}
|
|
121
103
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
|
|
105
|
+
if (matchingExpectations.length > 1) {
|
|
106
|
+
multipleMatches = true;
|
|
107
|
+
} else if (matchingExpectations.length === 1) {
|
|
108
|
+
expectedTargetPath = matchingExpectations[0].targetPath;
|
|
109
|
+
expectationType = matchingExpectations[0].type;
|
|
110
|
+
} else if (selectorMismatch) {
|
|
111
|
+
skips.push({
|
|
112
|
+
code: 'SELECTOR_MISMATCH',
|
|
113
|
+
message: 'Expectations exist but selector mismatch and no safe fallback match',
|
|
126
114
|
interaction: {
|
|
127
115
|
type: interaction.type,
|
|
128
116
|
selector: interaction.selector,
|
|
129
117
|
label: interaction.label
|
|
130
|
-
},
|
|
131
|
-
reason: 'Form validation errors logged to console with no visible feedback',
|
|
132
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
133
|
-
evidence: {
|
|
134
|
-
before: beforeScreenshot,
|
|
135
|
-
after: afterScreenshot,
|
|
136
|
-
beforeUrl: beforeUrl,
|
|
137
|
-
afterUrl: afterUrl,
|
|
138
|
-
consoleErrors: consoleSummary.consoleErrorCount,
|
|
139
|
-
errorMessages: consoleSummary.lastErrors?.slice(0, 3)
|
|
140
118
|
}
|
|
141
|
-
}
|
|
142
|
-
|
|
119
|
+
});
|
|
120
|
+
interactionsSkippedNoExpectation++;
|
|
121
|
+
continue;
|
|
143
122
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
after: afterScreenshot,
|
|
158
|
-
beforeUrl: beforeUrl,
|
|
159
|
-
afterUrl: afterUrl,
|
|
160
|
-
networkActivity: networkSummary.totalRequests || 0,
|
|
161
|
-
hadFeedback: uiSignalChanges.changed || false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!expectedTargetPath && manifest.projectType === 'react_spa' && interaction.type === 'link') {
|
|
127
|
+
const beforePath = getUrlPath(beforeUrl);
|
|
128
|
+
if (beforePath) {
|
|
129
|
+
const matchingRoutes = [];
|
|
130
|
+
const unreachableRoutes = new Set();
|
|
131
|
+
|
|
132
|
+
if (validation && validation.details) {
|
|
133
|
+
for (const detail of validation.details) {
|
|
134
|
+
if (detail.status === 'UNREACHABLE') {
|
|
135
|
+
unreachableRoutes.add(detail.path);
|
|
162
136
|
}
|
|
163
|
-
}
|
|
164
|
-
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const route of manifest.routes) {
|
|
141
|
+
if (!route.public) continue;
|
|
142
|
+
if (unreachableRoutes.has(route.path)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const routePath = route.path.toLowerCase();
|
|
146
|
+
const routeName = routePath.split('/').pop() || 'home';
|
|
147
|
+
const interactionLabel = (interaction.label || '').toLowerCase().trim();
|
|
148
|
+
|
|
149
|
+
if (interactionLabel.includes(routeName) || routeName.includes(interactionLabel)) {
|
|
150
|
+
matchingRoutes.push(route.path);
|
|
151
|
+
}
|
|
165
152
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
153
|
+
|
|
154
|
+
if (matchingRoutes.length > 1) {
|
|
155
|
+
skips.push({
|
|
156
|
+
code: 'AMBIGUOUS_MATCH',
|
|
157
|
+
message: 'Multiple expectations could match; conservative approach requires single clear match',
|
|
170
158
|
interaction: {
|
|
171
159
|
type: interaction.type,
|
|
172
160
|
selector: interaction.selector,
|
|
173
161
|
label: interaction.label
|
|
174
|
-
},
|
|
175
|
-
reason: 'Expected form submission did not occur',
|
|
176
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
177
|
-
evidence: {
|
|
178
|
-
before: beforeScreenshot,
|
|
179
|
-
after: afterScreenshot,
|
|
180
|
-
beforeUrl: beforeUrl,
|
|
181
|
-
afterUrl: afterUrl
|
|
182
162
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
163
|
+
});
|
|
164
|
+
interactionsSkippedNoExpectation++;
|
|
165
|
+
continue;
|
|
166
|
+
} else if (matchingRoutes.length === 1) {
|
|
167
|
+
expectedTargetPath = matchingRoutes[0];
|
|
168
|
+
expectationType = 'navigation';
|
|
185
169
|
}
|
|
186
170
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (multipleMatches) {
|
|
174
|
+
skips.push({
|
|
175
|
+
code: 'AMBIGUOUS_MATCH',
|
|
176
|
+
message: 'Multiple expectations could match; conservative approach requires single clear match',
|
|
177
|
+
interaction: {
|
|
178
|
+
type: interaction.type,
|
|
179
|
+
selector: interaction.selector,
|
|
180
|
+
label: interaction.label
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
interactionsSkippedNoExpectation++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interactionsAnalyzed++;
|
|
188
|
+
|
|
189
|
+
const hasUrlChange = hasMeaningfulUrlChange(beforeUrl, afterUrl);
|
|
190
|
+
const hasVisibleChangeResult = hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir);
|
|
191
|
+
const hasDomChangeResult = hasDomChange(trace);
|
|
192
|
+
|
|
193
|
+
if (expectedTargetPath) {
|
|
194
|
+
const afterPath = getUrlPath(afterUrl);
|
|
195
|
+
const normalizedTarget = expectedTargetPath.replace(/\/$/, '') || '/';
|
|
196
|
+
const normalizedAfter = afterPath ? afterPath.replace(/\/$/, '') || '/' : '';
|
|
191
197
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
type: 'network_silent_failure',
|
|
198
|
+
if (expectationType === 'form_submission') {
|
|
199
|
+
if (normalizedAfter !== normalizedTarget && !hasUrlChange && !hasDomChangeResult) {
|
|
200
|
+
findings.push({
|
|
201
|
+
type: 'silent_failure',
|
|
197
202
|
interaction: {
|
|
198
203
|
type: interaction.type,
|
|
199
204
|
selector: interaction.selector,
|
|
200
205
|
label: interaction.label
|
|
201
206
|
},
|
|
202
|
-
reason: '
|
|
203
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
204
|
-
evidence: {
|
|
205
|
-
before: beforeScreenshot,
|
|
206
|
-
after: afterScreenshot,
|
|
207
|
-
beforeUrl: beforeUrl,
|
|
208
|
-
afterUrl: afterUrl,
|
|
209
|
-
networkErrors: networkSummary.failedRequests,
|
|
210
|
-
topFailedUrls: networkSummary.topFailedUrls
|
|
211
|
-
}
|
|
212
|
-
}, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
|
|
213
|
-
findings.push(finding);
|
|
214
|
-
} else if (consoleSummary.hasErrors) {
|
|
215
|
-
const finding = createFindingWithConfidence({
|
|
216
|
-
type: 'validation_silent_failure',
|
|
217
|
-
interaction: {
|
|
218
|
-
type: interaction.type,
|
|
219
|
-
selector: interaction.selector,
|
|
220
|
-
label: interaction.label
|
|
221
|
-
},
|
|
222
|
-
reason: 'Navigation blocked by client-side validation errors',
|
|
223
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
224
|
-
evidence: {
|
|
225
|
-
before: beforeScreenshot,
|
|
226
|
-
after: afterScreenshot,
|
|
227
|
-
beforeUrl: beforeUrl,
|
|
228
|
-
afterUrl: afterUrl,
|
|
229
|
-
consoleErrors: consoleSummary.consoleErrorCount,
|
|
230
|
-
errorMessages: consoleSummary.lastErrors?.slice(0, 3)
|
|
231
|
-
}
|
|
232
|
-
}, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
|
|
233
|
-
findings.push(finding);
|
|
234
|
-
} else if (networkSummary.slowRequestsCount > 0 && !uiSignalChanges.changed) {
|
|
235
|
-
const finding = createFindingWithConfidence({
|
|
236
|
-
type: 'missing_feedback_failure',
|
|
237
|
-
interaction: {
|
|
238
|
-
type: interaction.type,
|
|
239
|
-
selector: interaction.selector,
|
|
240
|
-
label: interaction.label
|
|
241
|
-
},
|
|
242
|
-
reason: 'Slow navigation request with no loading indicator',
|
|
243
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
207
|
+
reason: 'Expected form submission did not occur',
|
|
244
208
|
evidence: {
|
|
245
209
|
before: beforeScreenshot,
|
|
246
210
|
after: afterScreenshot,
|
|
247
211
|
beforeUrl: beforeUrl,
|
|
248
|
-
afterUrl: afterUrl
|
|
249
|
-
slowRequests: networkSummary.slowRequestsCount,
|
|
250
|
-
slowRequestDurations: networkSummary.slowRequests?.map(r => r.duration)
|
|
212
|
+
afterUrl: afterUrl
|
|
251
213
|
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
} else if (expectationType === 'navigation') {
|
|
217
|
+
const urlMatchesTarget = normalizedAfter === normalizedTarget;
|
|
218
|
+
const hasEffect = urlMatchesTarget || hasVisibleChangeResult || hasDomChangeResult;
|
|
219
|
+
|
|
220
|
+
if (!hasEffect) {
|
|
221
|
+
findings.push({
|
|
222
|
+
type: 'silent_failure',
|
|
257
223
|
interaction: {
|
|
258
224
|
type: interaction.type,
|
|
259
225
|
selector: interaction.selector,
|
|
260
226
|
label: interaction.label
|
|
261
227
|
},
|
|
262
228
|
reason: 'Expected user-visible outcome did not occur',
|
|
263
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
264
229
|
evidence: {
|
|
265
230
|
before: beforeScreenshot,
|
|
266
231
|
after: afterScreenshot,
|
|
267
232
|
beforeUrl: beforeUrl,
|
|
268
233
|
afterUrl: afterUrl
|
|
269
234
|
}
|
|
270
|
-
}
|
|
271
|
-
findings.push(finding);
|
|
235
|
+
});
|
|
272
236
|
}
|
|
273
237
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// Code promised a fetch/axios call but no network activity detected
|
|
279
|
-
const finding = createFindingWithConfidence({
|
|
280
|
-
type: 'missing_network_action',
|
|
281
|
-
interaction: {
|
|
282
|
-
type: interaction.type,
|
|
283
|
-
selector: interaction.selector,
|
|
284
|
-
label: interaction.label
|
|
285
|
-
},
|
|
286
|
-
reason: 'Code contract promises network request but none was made',
|
|
287
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
288
|
-
evidence: {
|
|
289
|
-
before: beforeScreenshot,
|
|
290
|
-
after: afterScreenshot,
|
|
291
|
-
beforeUrl: beforeUrl,
|
|
292
|
-
afterUrl: afterUrl,
|
|
293
|
-
expectedMethod: expectationInfo.method,
|
|
294
|
-
expectedUrl: expectationInfo.urlPath,
|
|
295
|
-
sourceRef: attemptMeta.sourceRef
|
|
296
|
-
}
|
|
297
|
-
}, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
|
|
298
|
-
findings.push(finding);
|
|
299
|
-
}
|
|
300
|
-
// Check for network errors without feedback
|
|
301
|
-
else if (networkSummary.failedRequests > 0 && !uiSignalChanges.changed) {
|
|
302
|
-
const finding = createFindingWithConfidence({
|
|
303
|
-
type: 'network_silent_failure',
|
|
304
|
-
interaction: {
|
|
305
|
-
type: interaction.type,
|
|
306
|
-
selector: interaction.selector,
|
|
307
|
-
label: interaction.label
|
|
308
|
-
},
|
|
309
|
-
reason: 'Network action failed with no UI feedback',
|
|
310
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
311
|
-
evidence: {
|
|
312
|
-
before: beforeScreenshot,
|
|
313
|
-
after: afterScreenshot,
|
|
314
|
-
beforeUrl: beforeUrl,
|
|
315
|
-
afterUrl: afterUrl,
|
|
316
|
-
networkErrors: networkSummary.failedRequests,
|
|
317
|
-
failedByStatus: networkSummary.failedByStatus,
|
|
318
|
-
topFailedUrls: networkSummary.topFailedUrls,
|
|
319
|
-
expectedMethod: expectationInfo.method,
|
|
320
|
-
expectedUrl: expectationInfo.urlPath,
|
|
321
|
-
sourceRef: attemptMeta.sourceRef
|
|
322
|
-
}
|
|
323
|
-
}, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
|
|
324
|
-
findings.push(finding);
|
|
325
|
-
}
|
|
326
|
-
// Check for missing feedback on slow requests
|
|
327
|
-
else if (networkSummary.slowRequestsCount > 0 && !uiSignalChanges.changed) {
|
|
328
|
-
const finding = createFindingWithConfidence({
|
|
329
|
-
type: 'missing_feedback_failure',
|
|
330
|
-
interaction: {
|
|
331
|
-
type: interaction.type,
|
|
332
|
-
selector: interaction.selector,
|
|
333
|
-
label: interaction.label
|
|
334
|
-
},
|
|
335
|
-
reason: 'Slow network action with no loading feedback',
|
|
336
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
337
|
-
evidence: {
|
|
338
|
-
before: beforeScreenshot,
|
|
339
|
-
after: afterScreenshot,
|
|
340
|
-
beforeUrl: beforeUrl,
|
|
341
|
-
afterUrl: afterUrl,
|
|
342
|
-
slowRequests: networkSummary.slowRequestsCount,
|
|
343
|
-
expectedMethod: expectationInfo.method,
|
|
344
|
-
expectedUrl: expectationInfo.urlPath,
|
|
345
|
-
sourceRef: attemptMeta.sourceRef
|
|
346
|
-
}
|
|
347
|
-
}, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
|
|
348
|
-
findings.push(finding);
|
|
349
|
-
}
|
|
350
|
-
} else if (expectationType === 'state_action') {
|
|
351
|
-
// Wave 8: State action expectations from AST state contracts
|
|
352
|
-
const stateUIData = sensorData.stateUI || {};
|
|
353
|
-
const stateChanged = stateUIData.changed === true;
|
|
354
|
-
|
|
355
|
-
// Check if promised state mutation actually occurred
|
|
356
|
-
if (!stateChanged && !hasUrlChange && !hasDomChangeResult && networkSummary.totalRequests === 0) {
|
|
357
|
-
const finding = createFindingWithConfidence({
|
|
358
|
-
type: 'missing_state_action',
|
|
238
|
+
} else {
|
|
239
|
+
if (!hasUrlChange && !hasVisibleChangeResult && !hasDomChangeResult) {
|
|
240
|
+
findings.push({
|
|
241
|
+
type: 'silent_failure',
|
|
359
242
|
interaction: {
|
|
360
243
|
type: interaction.type,
|
|
361
244
|
selector: interaction.selector,
|
|
362
245
|
label: interaction.label
|
|
363
246
|
},
|
|
364
|
-
reason:
|
|
365
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
247
|
+
reason: 'Expected user-visible outcome did not occur',
|
|
366
248
|
evidence: {
|
|
367
249
|
before: beforeScreenshot,
|
|
368
250
|
after: afterScreenshot,
|
|
369
251
|
beforeUrl: beforeUrl,
|
|
370
|
-
afterUrl: afterUrl
|
|
371
|
-
expectedStateKind: expectationInfo.stateKind,
|
|
372
|
-
sourceRef: attemptMeta.sourceRef,
|
|
373
|
-
handlerRef: attemptMeta.handlerRef,
|
|
374
|
-
stateUIReasons: stateUIData.reasons || []
|
|
252
|
+
afterUrl: afterUrl
|
|
375
253
|
}
|
|
376
|
-
}
|
|
377
|
-
findings.push(finding);
|
|
254
|
+
});
|
|
378
255
|
}
|
|
379
256
|
}
|
|
380
257
|
}
|
|
381
258
|
|
|
382
|
-
|
|
259
|
+
// Interactive and accessibility intelligence
|
|
260
|
+
detectInteractiveFindings(observation.traces, manifest, findings);
|
|
261
|
+
|
|
262
|
+
// Infer canonical run directory from tracesPath when available
|
|
263
|
+
let runDir = null;
|
|
264
|
+
try {
|
|
265
|
+
runDir = dirname(tracesPath);
|
|
266
|
+
} catch {}
|
|
267
|
+
|
|
268
|
+
const findingsResult = writeFindings(projectDir, observation.url, findings, [], runDir);
|
|
383
269
|
|
|
384
270
|
const skipSummary = collectSkipReasons(skips);
|
|
385
271
|
|