@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
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import {
|
|
2
|
+
import { dirname, basename } 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
/**
|
|
11
|
+
* @param {string} manifestPath
|
|
12
|
+
* @param {string} tracesPath
|
|
13
|
+
* @param {Object} [validation]
|
|
14
|
+
* @returns {Promise<any>}
|
|
15
|
+
*/
|
|
16
|
+
export async function detect(manifestPath, tracesPath, validation = null, _expectationCoverageGaps = null, _silenceTracker = null) {
|
|
27
17
|
if (!existsSync(manifestPath)) {
|
|
28
18
|
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
29
19
|
}
|
|
@@ -41,8 +31,21 @@ export async function detect(manifestPath, tracesPath, validation = null, artifa
|
|
|
41
31
|
const projectDir = manifest.projectDir;
|
|
42
32
|
const findings = [];
|
|
43
33
|
|
|
34
|
+
// Extract runId from tracesPath: .verax/runs/<runId>/observation-traces.json
|
|
35
|
+
let runId = null;
|
|
36
|
+
try {
|
|
37
|
+
const runDir = dirname(tracesPath);
|
|
38
|
+
const runDirBasename = basename(runDir);
|
|
39
|
+
// Check if runDir is in .verax/runs/<runId> structure
|
|
40
|
+
const parentDir = dirname(runDir);
|
|
41
|
+
if (basename(parentDir) === 'runs' && basename(dirname(parentDir)) === '.verax') {
|
|
42
|
+
runId = runDirBasename;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore path parsing errors
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
let interactionsAnalyzed = 0;
|
|
45
|
-
let interactionsSkippedNoExpectation = 0;
|
|
46
49
|
const skips = [];
|
|
47
50
|
|
|
48
51
|
for (const trace of observation.traces) {
|
|
@@ -52,334 +55,243 @@ export async function detect(manifestPath, tracesPath, validation = null, artifa
|
|
|
52
55
|
const beforeScreenshot = trace.before.screenshot;
|
|
53
56
|
const afterScreenshot = trace.after.screenshot;
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
const attemptMeta = trace.meta || {};
|
|
57
|
-
const expectationInfo = getExpectation(manifest, interaction, beforeUrl, attemptMeta);
|
|
58
|
+
const expectsNav = expectsNavigation(manifest, interaction, beforeUrl);
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
skips.push(skipReason);
|
|
60
|
+
if (!expectsNav) {
|
|
61
|
+
const skipReason = classifySkipReason(manifest, interaction, beforeUrl, validation);
|
|
62
|
+
if (skipReason) {
|
|
63
|
+
skips.push({
|
|
64
|
+
code: skipReason.code,
|
|
65
|
+
message: skipReason.message,
|
|
66
|
+
interaction: {
|
|
67
|
+
type: interaction.type,
|
|
68
|
+
selector: interaction.selector,
|
|
69
|
+
label: interaction.label
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
let expectedTargetPath = null;
|
|
77
|
+
let expectationType = null;
|
|
78
|
+
let selectorMismatch = false;
|
|
79
|
+
let multipleMatches = false;
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
topFailedUrls: networkSummary.topFailedUrls
|
|
81
|
+
if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
|
|
82
|
+
const beforePath = getUrlPath(beforeUrl);
|
|
83
|
+
if (beforePath) {
|
|
84
|
+
const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
|
|
85
|
+
const matchingExpectations = [];
|
|
86
|
+
|
|
87
|
+
for (const expectation of manifest.staticExpectations) {
|
|
88
|
+
const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
|
|
89
|
+
if (normalizedFrom === normalizedBefore) {
|
|
90
|
+
const selectorHint = expectation.selectorHint || '';
|
|
91
|
+
const interactionSelector = interaction.selector || '';
|
|
92
|
+
|
|
93
|
+
if (selectorHint && interactionSelector) {
|
|
94
|
+
const normalizedSelectorHint = selectorHint.replace(/[[\]()]/g, '');
|
|
95
|
+
const normalizedInteractionSelector = interactionSelector.replace(/[[\]()]/g, '');
|
|
96
|
+
|
|
97
|
+
if (selectorHint === interactionSelector ||
|
|
98
|
+
selectorHint.includes(interactionSelector) ||
|
|
99
|
+
interactionSelector.includes(normalizedSelectorHint) ||
|
|
100
|
+
normalizedSelectorHint === normalizedInteractionSelector) {
|
|
101
|
+
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
102
|
+
matchingExpectations.push(expectation);
|
|
103
|
+
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
104
|
+
matchingExpectations.push(expectation);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
108
|
+
selectorMismatch = true;
|
|
109
|
+
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
110
|
+
selectorMismatch = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else if (!selectorHint && !interactionSelector) {
|
|
114
|
+
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
115
|
+
matchingExpectations.push(expectation);
|
|
116
|
+
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
117
|
+
matchingExpectations.push(expectation);
|
|
118
|
+
}
|
|
118
119
|
}
|
|
119
|
-
}
|
|
120
|
-
findings.push(finding);
|
|
120
|
+
}
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
|
|
123
|
+
if (matchingExpectations.length > 1) {
|
|
124
|
+
multipleMatches = true;
|
|
125
|
+
} else if (matchingExpectations.length === 1) {
|
|
126
|
+
expectedTargetPath = matchingExpectations[0].targetPath;
|
|
127
|
+
expectationType = matchingExpectations[0].type;
|
|
128
|
+
} else if (selectorMismatch) {
|
|
129
|
+
skips.push({
|
|
130
|
+
code: 'SELECTOR_MISMATCH',
|
|
131
|
+
message: 'Expectations exist but selector mismatch and no safe fallback match',
|
|
126
132
|
interaction: {
|
|
127
133
|
type: interaction.type,
|
|
128
134
|
selector: interaction.selector,
|
|
129
135
|
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
136
|
}
|
|
141
|
-
}
|
|
142
|
-
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
143
139
|
}
|
|
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
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!expectedTargetPath && manifest.projectType === 'react_spa' && interaction.type === 'link') {
|
|
144
|
+
const beforePath = getUrlPath(beforeUrl);
|
|
145
|
+
if (beforePath) {
|
|
146
|
+
const matchingRoutes = [];
|
|
147
|
+
const unreachableRoutes = new Set();
|
|
148
|
+
|
|
149
|
+
if (validation && validation.details) {
|
|
150
|
+
for (const detail of validation.details) {
|
|
151
|
+
if (detail.status === 'UNREACHABLE') {
|
|
152
|
+
unreachableRoutes.add(detail.path);
|
|
162
153
|
}
|
|
163
|
-
}
|
|
164
|
-
findings.push(finding);
|
|
154
|
+
}
|
|
165
155
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
156
|
+
|
|
157
|
+
for (const route of manifest.routes) {
|
|
158
|
+
if (!route.public) continue;
|
|
159
|
+
if (unreachableRoutes.has(route.path)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const routePath = route.path.toLowerCase();
|
|
163
|
+
const routeName = routePath.split('/').pop() || 'home';
|
|
164
|
+
const interactionLabel = (interaction.label || '').toLowerCase().trim();
|
|
165
|
+
|
|
166
|
+
if (interactionLabel.includes(routeName) || routeName.includes(interactionLabel)) {
|
|
167
|
+
matchingRoutes.push(route.path);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (matchingRoutes.length > 1) {
|
|
172
|
+
skips.push({
|
|
173
|
+
code: 'AMBIGUOUS_MATCH',
|
|
174
|
+
message: 'Multiple expectations could match; conservative approach requires single clear match',
|
|
170
175
|
interaction: {
|
|
171
176
|
type: interaction.type,
|
|
172
177
|
selector: interaction.selector,
|
|
173
178
|
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
179
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
} else if (matchingRoutes.length === 1) {
|
|
183
|
+
expectedTargetPath = matchingRoutes[0];
|
|
184
|
+
expectationType = 'navigation';
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (multipleMatches) {
|
|
190
|
+
skips.push({
|
|
191
|
+
code: 'AMBIGUOUS_MATCH',
|
|
192
|
+
message: 'Multiple expectations could match; conservative approach requires single clear match',
|
|
193
|
+
interaction: {
|
|
194
|
+
type: interaction.type,
|
|
195
|
+
selector: interaction.selector,
|
|
196
|
+
label: interaction.label
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interactionsAnalyzed++;
|
|
203
|
+
|
|
204
|
+
const hasUrlChange = hasMeaningfulUrlChange(beforeUrl, afterUrl);
|
|
205
|
+
// hasVisibleChange requires runId, skip comparison if runId unavailable
|
|
206
|
+
let hasVisibleChangeResult = false;
|
|
207
|
+
if (runId) {
|
|
208
|
+
try {
|
|
209
|
+
hasVisibleChangeResult = hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir, runId);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
// If screenshot comparison fails, treat as no visible change
|
|
212
|
+
hasVisibleChangeResult = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const hasDomChangeResult = hasDomChange(trace);
|
|
216
|
+
|
|
217
|
+
if (expectedTargetPath) {
|
|
218
|
+
const afterPath = getUrlPath(afterUrl);
|
|
219
|
+
const normalizedTarget = expectedTargetPath.replace(/\/$/, '') || '/';
|
|
220
|
+
const normalizedAfter = afterPath ? afterPath.replace(/\/$/, '') || '/' : '';
|
|
191
221
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
type: 'network_silent_failure',
|
|
222
|
+
if (expectationType === 'form_submission') {
|
|
223
|
+
if (normalizedAfter !== normalizedTarget && !hasUrlChange && !hasDomChangeResult) {
|
|
224
|
+
findings.push({
|
|
225
|
+
type: 'silent_failure',
|
|
197
226
|
interaction: {
|
|
198
227
|
type: interaction.type,
|
|
199
228
|
selector: interaction.selector,
|
|
200
229
|
label: interaction.label
|
|
201
230
|
},
|
|
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,
|
|
231
|
+
reason: 'Expected form submission did not occur',
|
|
244
232
|
evidence: {
|
|
245
233
|
before: beforeScreenshot,
|
|
246
234
|
after: afterScreenshot,
|
|
247
235
|
beforeUrl: beforeUrl,
|
|
248
|
-
afterUrl: afterUrl
|
|
249
|
-
slowRequests: networkSummary.slowRequestsCount,
|
|
250
|
-
slowRequestDurations: networkSummary.slowRequests?.map(r => r.duration)
|
|
236
|
+
afterUrl: afterUrl
|
|
251
237
|
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
} else if (expectationType === 'navigation') {
|
|
241
|
+
const urlMatchesTarget = normalizedAfter === normalizedTarget;
|
|
242
|
+
const hasEffect = urlMatchesTarget || hasVisibleChangeResult || hasDomChangeResult;
|
|
243
|
+
|
|
244
|
+
if (!hasEffect) {
|
|
245
|
+
findings.push({
|
|
246
|
+
type: 'silent_failure',
|
|
257
247
|
interaction: {
|
|
258
248
|
type: interaction.type,
|
|
259
249
|
selector: interaction.selector,
|
|
260
250
|
label: interaction.label
|
|
261
251
|
},
|
|
262
252
|
reason: 'Expected user-visible outcome did not occur',
|
|
263
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
264
253
|
evidence: {
|
|
265
254
|
before: beforeScreenshot,
|
|
266
255
|
after: afterScreenshot,
|
|
267
256
|
beforeUrl: beforeUrl,
|
|
268
257
|
afterUrl: afterUrl
|
|
269
258
|
}
|
|
270
|
-
}
|
|
271
|
-
findings.push(finding);
|
|
259
|
+
});
|
|
272
260
|
}
|
|
273
261
|
}
|
|
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',
|
|
262
|
+
} else {
|
|
263
|
+
if (!hasUrlChange && !hasVisibleChangeResult && !hasDomChangeResult) {
|
|
264
|
+
findings.push({
|
|
265
|
+
type: 'silent_failure',
|
|
359
266
|
interaction: {
|
|
360
267
|
type: interaction.type,
|
|
361
268
|
selector: interaction.selector,
|
|
362
269
|
label: interaction.label
|
|
363
270
|
},
|
|
364
|
-
reason:
|
|
365
|
-
expectationProof: ExpectationProof.PROVEN_EXPECTATION,
|
|
271
|
+
reason: 'Expected user-visible outcome did not occur',
|
|
366
272
|
evidence: {
|
|
367
273
|
before: beforeScreenshot,
|
|
368
274
|
after: afterScreenshot,
|
|
369
275
|
beforeUrl: beforeUrl,
|
|
370
|
-
afterUrl: afterUrl
|
|
371
|
-
expectedStateKind: expectationInfo.stateKind,
|
|
372
|
-
sourceRef: attemptMeta.sourceRef,
|
|
373
|
-
handlerRef: attemptMeta.handlerRef,
|
|
374
|
-
stateUIReasons: stateUIData.reasons || []
|
|
276
|
+
afterUrl: afterUrl
|
|
375
277
|
}
|
|
376
|
-
}
|
|
377
|
-
findings.push(finding);
|
|
278
|
+
});
|
|
378
279
|
}
|
|
379
280
|
}
|
|
380
281
|
}
|
|
381
282
|
|
|
382
|
-
|
|
283
|
+
// Interactive and accessibility intelligence
|
|
284
|
+
detectInteractiveFindings(observation.traces, manifest, findings);
|
|
285
|
+
|
|
286
|
+
// Infer canonical run directory from tracesPath when available
|
|
287
|
+
let runDir = null;
|
|
288
|
+
try {
|
|
289
|
+
runDir = dirname(tracesPath);
|
|
290
|
+
} catch {
|
|
291
|
+
// Ignore path parsing errors
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const findingsResult = writeFindings(projectDir, observation.url, findings, [], runDir);
|
|
383
295
|
|
|
384
296
|
const skipSummary = collectSkipReasons(skips);
|
|
385
297
|
|