@odavl/guardian 2.0.0 → 2.0.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/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
|
@@ -1,399 +1,399 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error & Failure Messaging — Human-Readable Error Output
|
|
3
|
-
*
|
|
4
|
-
* Classifies execution failures into canonical categories and provides
|
|
5
|
-
* plain-language explanations with actionable next steps.
|
|
6
|
-
* Production-grade DX improvement for CLI output.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Canonical error taxonomy (internal-only classification)
|
|
10
|
-
const ERROR_CATEGORIES = {
|
|
11
|
-
TIMEOUT: 'TIMEOUT',
|
|
12
|
-
ELEMENT_NOT_FOUND: 'ELEMENT_NOT_FOUND',
|
|
13
|
-
NAVIGATION_FAILED: 'NAVIGATION_FAILED',
|
|
14
|
-
AUTH_BLOCKED: 'AUTH_BLOCKED',
|
|
15
|
-
NOT_APPLICABLE: 'NOT_APPLICABLE',
|
|
16
|
-
DISABLED_BY_PRESET: 'DISABLED_BY_PRESET',
|
|
17
|
-
USER_FILTERED: 'USER_FILTERED',
|
|
18
|
-
MISSING_DEPENDENCY: 'MISSING_DEPENDENCY',
|
|
19
|
-
INFRA_ERROR: 'INFRA_ERROR',
|
|
20
|
-
UNKNOWN: 'UNKNOWN'
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Error message templates (centralized, not duplicated)
|
|
24
|
-
const ERROR_MESSAGES = {
|
|
25
|
-
TIMEOUT: {
|
|
26
|
-
title: 'Timeout waiting for interaction',
|
|
27
|
-
explanation: 'This step took longer than expected to complete. The page may have performance issues or the timeout setting may be too strict.',
|
|
28
|
-
action: 'Increase timeout setting or verify page performance.'
|
|
29
|
-
},
|
|
30
|
-
ELEMENT_NOT_FOUND: {
|
|
31
|
-
title: 'Expected element not found',
|
|
32
|
-
explanation: 'A critical UI element (button, form, link) was not present on the page. The page structure may have changed or the element is dynamically loaded.',
|
|
33
|
-
action: 'Verify the element selector is correct or wait for dynamic content to load.'
|
|
34
|
-
},
|
|
35
|
-
NAVIGATION_FAILED: {
|
|
36
|
-
title: 'Navigation failed',
|
|
37
|
-
explanation: 'Attempting to navigate to a URL resulted in an error. The page may be unavailable, blocked, or returned an error status.',
|
|
38
|
-
action: 'Check if the URL is accessible and returns a valid response (HTTP 200).'
|
|
39
|
-
},
|
|
40
|
-
AUTH_BLOCKED: {
|
|
41
|
-
title: 'Authentication blocked',
|
|
42
|
-
explanation: 'The test was blocked by authentication or access control. The credentials may be incorrect or the account may lack permissions.',
|
|
43
|
-
action: 'Verify credentials and ensure the test account has required permissions.'
|
|
44
|
-
},
|
|
45
|
-
NOT_APPLICABLE: {
|
|
46
|
-
title: 'Skipped (not applicable)',
|
|
47
|
-
explanation: 'This step is not applicable to this site based on its capabilities or configuration.',
|
|
48
|
-
action: 'This is expected behavior. No action required.'
|
|
49
|
-
},
|
|
50
|
-
DISABLED_BY_PRESET: {
|
|
51
|
-
title: 'Skipped (disabled by preset)',
|
|
52
|
-
explanation: 'This step is disabled in the selected testing preset. It can be re-enabled by choosing a different preset.',
|
|
53
|
-
action: 'Choose a different preset if you want to test this flow, or create a custom configuration.'
|
|
54
|
-
},
|
|
55
|
-
USER_FILTERED: {
|
|
56
|
-
title: 'Skipped (user filtered)',
|
|
57
|
-
explanation: 'This step was explicitly filtered out in your configuration.',
|
|
58
|
-
action: 'Update your configuration if you want to include this step in testing.'
|
|
59
|
-
},
|
|
60
|
-
MISSING_DEPENDENCY: {
|
|
61
|
-
title: 'Skipped (missing dependency)',
|
|
62
|
-
explanation: 'This step requires a previous step to pass, but that step did not execute or failed.',
|
|
63
|
-
action: 'Fix the blocking issue in the previous step, or run tests without dependencies.'
|
|
64
|
-
},
|
|
65
|
-
INFRA_ERROR: {
|
|
66
|
-
title: 'Infrastructure error',
|
|
67
|
-
explanation: 'A system-level issue occurred (browser launch failed, permissions error, etc.). This is not related to your site.',
|
|
68
|
-
action: 'Check system resources, permissions, and Guardian logs. Retry the test.'
|
|
69
|
-
},
|
|
70
|
-
UNKNOWN: {
|
|
71
|
-
title: 'Unexpected error',
|
|
72
|
-
explanation: 'An error occurred that does not fit common categories. See detailed logs for more information.',
|
|
73
|
-
action: 'Check Guardian logs with GUARDIAN_DEBUG=1 for full error details.'
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Skip messages (NOT errors, labeled clearly)
|
|
78
|
-
const SKIP_MESSAGES = {
|
|
79
|
-
DISABLED_BY_PRESET: 'Skipped by preset configuration',
|
|
80
|
-
NOT_APPLICABLE: 'Not applicable to this site',
|
|
81
|
-
USER_FILTERED: 'User-filtered from testing',
|
|
82
|
-
MISSING_DEPENDENCY: 'Skipped (blocking flow failed)'
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Classify a raw error/failure into canonical category
|
|
87
|
-
*
|
|
88
|
-
* @param {Object} failure - Failure object with outcome, error, reason, etc.
|
|
89
|
-
* @returns {string} Canonical error category
|
|
90
|
-
*/
|
|
91
|
-
function classifyError(failure = {}) {
|
|
92
|
-
const {
|
|
93
|
-
outcome,
|
|
94
|
-
reason,
|
|
95
|
-
failureReason,
|
|
96
|
-
message,
|
|
97
|
-
code
|
|
98
|
-
} = failure;
|
|
99
|
-
|
|
100
|
-
// Skip outcomes (NOT errors)
|
|
101
|
-
if (outcome === 'NOT_APPLICABLE') return ERROR_CATEGORIES.NOT_APPLICABLE;
|
|
102
|
-
if (outcome === 'SKIPPED') {
|
|
103
|
-
if (reason === 'DISABLED_BY_PRESET') return ERROR_CATEGORIES.DISABLED_BY_PRESET;
|
|
104
|
-
if (reason === 'USER_FILTERED') return ERROR_CATEGORIES.USER_FILTERED;
|
|
105
|
-
if (reason === 'MISSING_DEPENDENCY') return ERROR_CATEGORIES.MISSING_DEPENDENCY;
|
|
106
|
-
return ERROR_CATEGORIES.NOT_APPLICABLE;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Timeout errors
|
|
110
|
-
if (failureReason === 'TIMEOUT' || reason === 'TIMEOUT') {
|
|
111
|
-
return ERROR_CATEGORIES.TIMEOUT;
|
|
112
|
-
}
|
|
113
|
-
if (message && message.toLowerCase().includes('timeout')) {
|
|
114
|
-
return ERROR_CATEGORIES.TIMEOUT;
|
|
115
|
-
}
|
|
116
|
-
if (code === 'TIMEOUT') {
|
|
117
|
-
return ERROR_CATEGORIES.TIMEOUT;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Element not found
|
|
121
|
-
if (failureReason === 'ELEMENT_NOT_FOUND' || reason === 'ELEMENT_NOT_FOUND') {
|
|
122
|
-
return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
|
|
123
|
-
}
|
|
124
|
-
if (message && (message.toLowerCase().includes('not found') || message.toLowerCase().includes('selector'))) {
|
|
125
|
-
return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
|
|
126
|
-
}
|
|
127
|
-
if (code === 'ELEMENT_NOT_FOUND') {
|
|
128
|
-
return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Navigation failures
|
|
132
|
-
if (failureReason === 'NAVIGATION_FAILED' || reason === 'NAVIGATION_FAILED') {
|
|
133
|
-
return ERROR_CATEGORIES.NAVIGATION_FAILED;
|
|
134
|
-
}
|
|
135
|
-
if (message && (message.toLowerCase().includes('navigation') || message.toLowerCase().includes('net::err'))) {
|
|
136
|
-
return ERROR_CATEGORIES.NAVIGATION_FAILED;
|
|
137
|
-
}
|
|
138
|
-
if (code === 'NAVIGATION_FAILED') {
|
|
139
|
-
return ERROR_CATEGORIES.NAVIGATION_FAILED;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Auth blocked
|
|
143
|
-
if (failureReason === 'AUTH_BLOCKED' || reason === 'AUTH_BLOCKED') {
|
|
144
|
-
return ERROR_CATEGORIES.AUTH_BLOCKED;
|
|
145
|
-
}
|
|
146
|
-
if (message && (message.toLowerCase().includes('unauthorized') || message.toLowerCase().includes('forbidden') || message.toLowerCase().includes('403') || message.toLowerCase().includes('401'))) {
|
|
147
|
-
return ERROR_CATEGORIES.AUTH_BLOCKED;
|
|
148
|
-
}
|
|
149
|
-
if (code === 'AUTH_BLOCKED') {
|
|
150
|
-
return ERROR_CATEGORIES.AUTH_BLOCKED;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Infrastructure errors
|
|
154
|
-
if (failureReason === 'INFRA_ERROR' || reason === 'INFRA_ERROR') {
|
|
155
|
-
return ERROR_CATEGORIES.INFRA_ERROR;
|
|
156
|
-
}
|
|
157
|
-
if (message && (message.toLowerCase().includes('browser') || message.toLowerCase().includes('permission') || message.toLowerCase().includes('system'))) {
|
|
158
|
-
return ERROR_CATEGORIES.INFRA_ERROR;
|
|
159
|
-
}
|
|
160
|
-
if (code === 'BROWSER_LAUNCH_FAILED' || code === 'PERMISSION_DENIED') {
|
|
161
|
-
return ERROR_CATEGORIES.INFRA_ERROR;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Missing dependency
|
|
165
|
-
if (failureReason === 'MISSING_DEPENDENCY' || reason === 'MISSING_DEPENDENCY') {
|
|
166
|
-
return ERROR_CATEGORIES.MISSING_DEPENDENCY;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Default to unknown
|
|
170
|
-
return ERROR_CATEGORIES.UNKNOWN;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Extract human-friendly error info from failure
|
|
175
|
-
*
|
|
176
|
-
* @param {Object} failure - Failure with outcome, error details, etc.
|
|
177
|
-
* @returns {Object} { category, title, explanation, action }
|
|
178
|
-
*/
|
|
179
|
-
function getErrorInfo(failure = {}) {
|
|
180
|
-
const category = classifyError(failure);
|
|
181
|
-
const template = ERROR_MESSAGES[category] || ERROR_MESSAGES[ERROR_CATEGORIES.UNKNOWN];
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
category,
|
|
185
|
-
title: template.title,
|
|
186
|
-
explanation: template.explanation,
|
|
187
|
-
action: template.action
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Check if output should be shown
|
|
193
|
-
* Skip in quiet, CI, or non-TTY environments
|
|
194
|
-
*
|
|
195
|
-
* @param {Object} config - Guardian config
|
|
196
|
-
* @param {Array} args - CLI arguments
|
|
197
|
-
* @returns {boolean} true if should show error clarity
|
|
198
|
-
*/
|
|
199
|
-
function shouldShowErrorClarity(config = {}, args = []) {
|
|
200
|
-
// Skip if --quiet or -q flag
|
|
201
|
-
if (args.includes('--quiet') || args.includes('-q')) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Skip if non-TTY (CI/automation without explicit output)
|
|
206
|
-
if (!process.stdout.isTTY) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Group failures by category
|
|
215
|
-
*
|
|
216
|
-
* @param {Array} failures - Array of failed attempts/flows
|
|
217
|
-
* @returns {Object} Map of category -> [failures]
|
|
218
|
-
*/
|
|
219
|
-
function groupFailuresByCategory(failures = []) {
|
|
220
|
-
const groups = {};
|
|
221
|
-
|
|
222
|
-
(failures || []).forEach(failure => {
|
|
223
|
-
const category = classifyError(failure);
|
|
224
|
-
if (!groups[category]) {
|
|
225
|
-
groups[category] = [];
|
|
226
|
-
}
|
|
227
|
-
groups[category].push(failure);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
return groups;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Deduplicate similar errors within a category
|
|
235
|
-
*
|
|
236
|
-
* @param {Array} failures - Array of failures in same category
|
|
237
|
-
* @returns {Array} Deduplicated failures (max 3)
|
|
238
|
-
*/
|
|
239
|
-
function deduplicateErrors(failures = []) {
|
|
240
|
-
const seen = new Set();
|
|
241
|
-
const deduplicated = [];
|
|
242
|
-
|
|
243
|
-
(failures || []).forEach(failure => {
|
|
244
|
-
const key = `${failure.attemptId || failure.name}`;
|
|
245
|
-
if (!seen.has(key) && deduplicated.length < 3) {
|
|
246
|
-
seen.add(key);
|
|
247
|
-
deduplicated.push(failure);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
return deduplicated;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Check if this is a skip (not an error)
|
|
256
|
-
*
|
|
257
|
-
* @param {string} category - Error category
|
|
258
|
-
* @returns {boolean} true if this is a skip
|
|
259
|
-
*/
|
|
260
|
-
function isSkip(category) {
|
|
261
|
-
return [
|
|
262
|
-
ERROR_CATEGORIES.NOT_APPLICABLE,
|
|
263
|
-
ERROR_CATEGORIES.DISABLED_BY_PRESET,
|
|
264
|
-
ERROR_CATEGORIES.USER_FILTERED,
|
|
265
|
-
ERROR_CATEGORIES.MISSING_DEPENDENCY
|
|
266
|
-
].includes(category);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Format error clarity block for CLI output
|
|
271
|
-
*
|
|
272
|
-
* @param {Array} failures - Failed attempts/flows
|
|
273
|
-
* @param {Object} config - Guardian config
|
|
274
|
-
* @param {Array} args - CLI arguments
|
|
275
|
-
* @returns {string} Formatted error clarity block
|
|
276
|
-
*/
|
|
277
|
-
function formatErrorClarity(failures = [], config = {}, args = []) {
|
|
278
|
-
if (!shouldShowErrorClarity(config, args)) {
|
|
279
|
-
return '';
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!failures || failures.length === 0) {
|
|
283
|
-
return '';
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const lines = [];
|
|
287
|
-
const groups = groupFailuresByCategory(failures);
|
|
288
|
-
|
|
289
|
-
// Separate actual errors from skips
|
|
290
|
-
const errors = {};
|
|
291
|
-
const skips = {};
|
|
292
|
-
|
|
293
|
-
Object.entries(groups).forEach(([category, categoryFailures]) => {
|
|
294
|
-
if (isSkip(category)) {
|
|
295
|
-
skips[category] = categoryFailures;
|
|
296
|
-
} else {
|
|
297
|
-
errors[category] = categoryFailures;
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// Print errors section
|
|
302
|
-
const errorCategories = Object.keys(errors);
|
|
303
|
-
if (errorCategories.length > 0) {
|
|
304
|
-
lines.push('');
|
|
305
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
306
|
-
lines.push('FAILURES & ERRORS');
|
|
307
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
308
|
-
lines.push('');
|
|
309
|
-
|
|
310
|
-
errorCategories.forEach(category => {
|
|
311
|
-
const categoryFailures = errors[category];
|
|
312
|
-
const errorInfo = getErrorInfo(categoryFailures[0]);
|
|
313
|
-
|
|
314
|
-
lines.push(`${errorInfo.title}`);
|
|
315
|
-
lines.push('────────────────────────────────────────────────────────────');
|
|
316
|
-
lines.push(`${errorInfo.explanation}`);
|
|
317
|
-
lines.push(`Action: ${errorInfo.action}`);
|
|
318
|
-
|
|
319
|
-
// List affected flows/attempts (max 3)
|
|
320
|
-
const deduped = deduplicateErrors(categoryFailures);
|
|
321
|
-
const names = deduped
|
|
322
|
-
.map(f => f.attemptName || f.name || f.attemptId || 'unknown');
|
|
323
|
-
|
|
324
|
-
if (names.length > 0) {
|
|
325
|
-
lines.push(`Affected: ${names.join(', ')}`);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (categoryFailures.length > 3) {
|
|
329
|
-
lines.push(`(+${categoryFailures.length - 3} more)`);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
lines.push('');
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Print skips section
|
|
337
|
-
const skipCategories = Object.keys(skips);
|
|
338
|
-
if (skipCategories.length > 0) {
|
|
339
|
-
lines.push('');
|
|
340
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
341
|
-
lines.push('SKIPPED ATTEMPTS');
|
|
342
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
343
|
-
lines.push('');
|
|
344
|
-
|
|
345
|
-
skipCategories.forEach(category => {
|
|
346
|
-
const categorySkips = skips[category];
|
|
347
|
-
const skipReason = SKIP_MESSAGES[category] || 'Skipped';
|
|
348
|
-
|
|
349
|
-
// Count skips in this category
|
|
350
|
-
const names = deduplicateErrors(categorySkips)
|
|
351
|
-
.map(s => s.attemptName || s.name || s.attemptId || 'unknown')
|
|
352
|
-
.slice(0, 3);
|
|
353
|
-
|
|
354
|
-
lines.push(`${skipReason} (${categorySkips.length})`);
|
|
355
|
-
lines.push('────────────────────────────────────────────────────────────');
|
|
356
|
-
if (names.length > 0) {
|
|
357
|
-
lines.push(`${names.join(', ')}`);
|
|
358
|
-
}
|
|
359
|
-
if (categorySkips.length > 3) {
|
|
360
|
-
lines.push(`+${categorySkips.length - 3} more`);
|
|
361
|
-
}
|
|
362
|
-
lines.push('');
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (lines.length > 1) {
|
|
367
|
-
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return lines.join('\n');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Print error clarity block to stdout
|
|
375
|
-
*
|
|
376
|
-
* @param {Array} failures - Failed attempts/flows
|
|
377
|
-
* @param {Object} config - Guardian config
|
|
378
|
-
* @param {Array} args - CLI arguments
|
|
379
|
-
*/
|
|
380
|
-
function printErrorClarity(failures = [], config = {}, args = []) {
|
|
381
|
-
const output = formatErrorClarity(failures, config, args);
|
|
382
|
-
if (output && output.trim().length > 0) {
|
|
383
|
-
console.log(output);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
module.exports = {
|
|
388
|
-
ERROR_CATEGORIES,
|
|
389
|
-
ERROR_MESSAGES,
|
|
390
|
-
SKIP_MESSAGES,
|
|
391
|
-
classifyError,
|
|
392
|
-
getErrorInfo,
|
|
393
|
-
shouldShowErrorClarity,
|
|
394
|
-
groupFailuresByCategory,
|
|
395
|
-
deduplicateErrors,
|
|
396
|
-
isSkip,
|
|
397
|
-
formatErrorClarity,
|
|
398
|
-
printErrorClarity
|
|
399
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Error & Failure Messaging — Human-Readable Error Output
|
|
3
|
+
*
|
|
4
|
+
* Classifies execution failures into canonical categories and provides
|
|
5
|
+
* plain-language explanations with actionable next steps.
|
|
6
|
+
* Production-grade DX improvement for CLI output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Canonical error taxonomy (internal-only classification)
|
|
10
|
+
const ERROR_CATEGORIES = {
|
|
11
|
+
TIMEOUT: 'TIMEOUT',
|
|
12
|
+
ELEMENT_NOT_FOUND: 'ELEMENT_NOT_FOUND',
|
|
13
|
+
NAVIGATION_FAILED: 'NAVIGATION_FAILED',
|
|
14
|
+
AUTH_BLOCKED: 'AUTH_BLOCKED',
|
|
15
|
+
NOT_APPLICABLE: 'NOT_APPLICABLE',
|
|
16
|
+
DISABLED_BY_PRESET: 'DISABLED_BY_PRESET',
|
|
17
|
+
USER_FILTERED: 'USER_FILTERED',
|
|
18
|
+
MISSING_DEPENDENCY: 'MISSING_DEPENDENCY',
|
|
19
|
+
INFRA_ERROR: 'INFRA_ERROR',
|
|
20
|
+
UNKNOWN: 'UNKNOWN'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Error message templates (centralized, not duplicated)
|
|
24
|
+
const ERROR_MESSAGES = {
|
|
25
|
+
TIMEOUT: {
|
|
26
|
+
title: 'Timeout waiting for interaction',
|
|
27
|
+
explanation: 'This step took longer than expected to complete. The page may have performance issues or the timeout setting may be too strict.',
|
|
28
|
+
action: 'Increase timeout setting or verify page performance.'
|
|
29
|
+
},
|
|
30
|
+
ELEMENT_NOT_FOUND: {
|
|
31
|
+
title: 'Expected element not found',
|
|
32
|
+
explanation: 'A critical UI element (button, form, link) was not present on the page. The page structure may have changed or the element is dynamically loaded.',
|
|
33
|
+
action: 'Verify the element selector is correct or wait for dynamic content to load.'
|
|
34
|
+
},
|
|
35
|
+
NAVIGATION_FAILED: {
|
|
36
|
+
title: 'Navigation failed',
|
|
37
|
+
explanation: 'Attempting to navigate to a URL resulted in an error. The page may be unavailable, blocked, or returned an error status.',
|
|
38
|
+
action: 'Check if the URL is accessible and returns a valid response (HTTP 200).'
|
|
39
|
+
},
|
|
40
|
+
AUTH_BLOCKED: {
|
|
41
|
+
title: 'Authentication blocked',
|
|
42
|
+
explanation: 'The test was blocked by authentication or access control. The credentials may be incorrect or the account may lack permissions.',
|
|
43
|
+
action: 'Verify credentials and ensure the test account has required permissions.'
|
|
44
|
+
},
|
|
45
|
+
NOT_APPLICABLE: {
|
|
46
|
+
title: 'Skipped (not applicable)',
|
|
47
|
+
explanation: 'This step is not applicable to this site based on its capabilities or configuration.',
|
|
48
|
+
action: 'This is expected behavior. No action required.'
|
|
49
|
+
},
|
|
50
|
+
DISABLED_BY_PRESET: {
|
|
51
|
+
title: 'Skipped (disabled by preset)',
|
|
52
|
+
explanation: 'This step is disabled in the selected testing preset. It can be re-enabled by choosing a different preset.',
|
|
53
|
+
action: 'Choose a different preset if you want to test this flow, or create a custom configuration.'
|
|
54
|
+
},
|
|
55
|
+
USER_FILTERED: {
|
|
56
|
+
title: 'Skipped (user filtered)',
|
|
57
|
+
explanation: 'This step was explicitly filtered out in your configuration.',
|
|
58
|
+
action: 'Update your configuration if you want to include this step in testing.'
|
|
59
|
+
},
|
|
60
|
+
MISSING_DEPENDENCY: {
|
|
61
|
+
title: 'Skipped (missing dependency)',
|
|
62
|
+
explanation: 'This step requires a previous step to pass, but that step did not execute or failed.',
|
|
63
|
+
action: 'Fix the blocking issue in the previous step, or run tests without dependencies.'
|
|
64
|
+
},
|
|
65
|
+
INFRA_ERROR: {
|
|
66
|
+
title: 'Infrastructure error',
|
|
67
|
+
explanation: 'A system-level issue occurred (browser launch failed, permissions error, etc.). This is not related to your site.',
|
|
68
|
+
action: 'Check system resources, permissions, and Guardian logs. Retry the test.'
|
|
69
|
+
},
|
|
70
|
+
UNKNOWN: {
|
|
71
|
+
title: 'Unexpected error',
|
|
72
|
+
explanation: 'An error occurred that does not fit common categories. See detailed logs for more information.',
|
|
73
|
+
action: 'Check Guardian logs with GUARDIAN_DEBUG=1 for full error details.'
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Skip messages (NOT errors, labeled clearly)
|
|
78
|
+
const SKIP_MESSAGES = {
|
|
79
|
+
DISABLED_BY_PRESET: 'Skipped by preset configuration',
|
|
80
|
+
NOT_APPLICABLE: 'Not applicable to this site',
|
|
81
|
+
USER_FILTERED: 'User-filtered from testing',
|
|
82
|
+
MISSING_DEPENDENCY: 'Skipped (blocking flow failed)'
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Classify a raw error/failure into canonical category
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} failure - Failure object with outcome, error, reason, etc.
|
|
89
|
+
* @returns {string} Canonical error category
|
|
90
|
+
*/
|
|
91
|
+
function classifyError(failure = {}) {
|
|
92
|
+
const {
|
|
93
|
+
outcome,
|
|
94
|
+
reason,
|
|
95
|
+
failureReason,
|
|
96
|
+
message,
|
|
97
|
+
code
|
|
98
|
+
} = failure;
|
|
99
|
+
|
|
100
|
+
// Skip outcomes (NOT errors)
|
|
101
|
+
if (outcome === 'NOT_APPLICABLE') return ERROR_CATEGORIES.NOT_APPLICABLE;
|
|
102
|
+
if (outcome === 'SKIPPED') {
|
|
103
|
+
if (reason === 'DISABLED_BY_PRESET') return ERROR_CATEGORIES.DISABLED_BY_PRESET;
|
|
104
|
+
if (reason === 'USER_FILTERED') return ERROR_CATEGORIES.USER_FILTERED;
|
|
105
|
+
if (reason === 'MISSING_DEPENDENCY') return ERROR_CATEGORIES.MISSING_DEPENDENCY;
|
|
106
|
+
return ERROR_CATEGORIES.NOT_APPLICABLE;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Timeout errors
|
|
110
|
+
if (failureReason === 'TIMEOUT' || reason === 'TIMEOUT') {
|
|
111
|
+
return ERROR_CATEGORIES.TIMEOUT;
|
|
112
|
+
}
|
|
113
|
+
if (message && message.toLowerCase().includes('timeout')) {
|
|
114
|
+
return ERROR_CATEGORIES.TIMEOUT;
|
|
115
|
+
}
|
|
116
|
+
if (code === 'TIMEOUT') {
|
|
117
|
+
return ERROR_CATEGORIES.TIMEOUT;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Element not found
|
|
121
|
+
if (failureReason === 'ELEMENT_NOT_FOUND' || reason === 'ELEMENT_NOT_FOUND') {
|
|
122
|
+
return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
|
|
123
|
+
}
|
|
124
|
+
if (message && (message.toLowerCase().includes('not found') || message.toLowerCase().includes('selector'))) {
|
|
125
|
+
return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
|
|
126
|
+
}
|
|
127
|
+
if (code === 'ELEMENT_NOT_FOUND') {
|
|
128
|
+
return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Navigation failures
|
|
132
|
+
if (failureReason === 'NAVIGATION_FAILED' || reason === 'NAVIGATION_FAILED') {
|
|
133
|
+
return ERROR_CATEGORIES.NAVIGATION_FAILED;
|
|
134
|
+
}
|
|
135
|
+
if (message && (message.toLowerCase().includes('navigation') || message.toLowerCase().includes('net::err'))) {
|
|
136
|
+
return ERROR_CATEGORIES.NAVIGATION_FAILED;
|
|
137
|
+
}
|
|
138
|
+
if (code === 'NAVIGATION_FAILED') {
|
|
139
|
+
return ERROR_CATEGORIES.NAVIGATION_FAILED;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Auth blocked
|
|
143
|
+
if (failureReason === 'AUTH_BLOCKED' || reason === 'AUTH_BLOCKED') {
|
|
144
|
+
return ERROR_CATEGORIES.AUTH_BLOCKED;
|
|
145
|
+
}
|
|
146
|
+
if (message && (message.toLowerCase().includes('unauthorized') || message.toLowerCase().includes('forbidden') || message.toLowerCase().includes('403') || message.toLowerCase().includes('401'))) {
|
|
147
|
+
return ERROR_CATEGORIES.AUTH_BLOCKED;
|
|
148
|
+
}
|
|
149
|
+
if (code === 'AUTH_BLOCKED') {
|
|
150
|
+
return ERROR_CATEGORIES.AUTH_BLOCKED;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Infrastructure errors
|
|
154
|
+
if (failureReason === 'INFRA_ERROR' || reason === 'INFRA_ERROR') {
|
|
155
|
+
return ERROR_CATEGORIES.INFRA_ERROR;
|
|
156
|
+
}
|
|
157
|
+
if (message && (message.toLowerCase().includes('browser') || message.toLowerCase().includes('permission') || message.toLowerCase().includes('system'))) {
|
|
158
|
+
return ERROR_CATEGORIES.INFRA_ERROR;
|
|
159
|
+
}
|
|
160
|
+
if (code === 'BROWSER_LAUNCH_FAILED' || code === 'PERMISSION_DENIED') {
|
|
161
|
+
return ERROR_CATEGORIES.INFRA_ERROR;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Missing dependency
|
|
165
|
+
if (failureReason === 'MISSING_DEPENDENCY' || reason === 'MISSING_DEPENDENCY') {
|
|
166
|
+
return ERROR_CATEGORIES.MISSING_DEPENDENCY;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Default to unknown
|
|
170
|
+
return ERROR_CATEGORIES.UNKNOWN;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extract human-friendly error info from failure
|
|
175
|
+
*
|
|
176
|
+
* @param {Object} failure - Failure with outcome, error details, etc.
|
|
177
|
+
* @returns {Object} { category, title, explanation, action }
|
|
178
|
+
*/
|
|
179
|
+
function getErrorInfo(failure = {}) {
|
|
180
|
+
const category = classifyError(failure);
|
|
181
|
+
const template = ERROR_MESSAGES[category] || ERROR_MESSAGES[ERROR_CATEGORIES.UNKNOWN];
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
category,
|
|
185
|
+
title: template.title,
|
|
186
|
+
explanation: template.explanation,
|
|
187
|
+
action: template.action
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if output should be shown
|
|
193
|
+
* Skip in quiet, CI, or non-TTY environments
|
|
194
|
+
*
|
|
195
|
+
* @param {Object} config - Guardian config
|
|
196
|
+
* @param {Array} args - CLI arguments
|
|
197
|
+
* @returns {boolean} true if should show error clarity
|
|
198
|
+
*/
|
|
199
|
+
function shouldShowErrorClarity(config = {}, args = []) {
|
|
200
|
+
// Skip if --quiet or -q flag
|
|
201
|
+
if (args.includes('--quiet') || args.includes('-q')) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Skip if non-TTY (CI/automation without explicit output)
|
|
206
|
+
if (!process.stdout.isTTY) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Group failures by category
|
|
215
|
+
*
|
|
216
|
+
* @param {Array} failures - Array of failed attempts/flows
|
|
217
|
+
* @returns {Object} Map of category -> [failures]
|
|
218
|
+
*/
|
|
219
|
+
function groupFailuresByCategory(failures = []) {
|
|
220
|
+
const groups = {};
|
|
221
|
+
|
|
222
|
+
(failures || []).forEach(failure => {
|
|
223
|
+
const category = classifyError(failure);
|
|
224
|
+
if (!groups[category]) {
|
|
225
|
+
groups[category] = [];
|
|
226
|
+
}
|
|
227
|
+
groups[category].push(failure);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return groups;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Deduplicate similar errors within a category
|
|
235
|
+
*
|
|
236
|
+
* @param {Array} failures - Array of failures in same category
|
|
237
|
+
* @returns {Array} Deduplicated failures (max 3)
|
|
238
|
+
*/
|
|
239
|
+
function deduplicateErrors(failures = []) {
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
const deduplicated = [];
|
|
242
|
+
|
|
243
|
+
(failures || []).forEach(failure => {
|
|
244
|
+
const key = `${failure.attemptId || failure.name}`;
|
|
245
|
+
if (!seen.has(key) && deduplicated.length < 3) {
|
|
246
|
+
seen.add(key);
|
|
247
|
+
deduplicated.push(failure);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return deduplicated;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if this is a skip (not an error)
|
|
256
|
+
*
|
|
257
|
+
* @param {string} category - Error category
|
|
258
|
+
* @returns {boolean} true if this is a skip
|
|
259
|
+
*/
|
|
260
|
+
function isSkip(category) {
|
|
261
|
+
return [
|
|
262
|
+
ERROR_CATEGORIES.NOT_APPLICABLE,
|
|
263
|
+
ERROR_CATEGORIES.DISABLED_BY_PRESET,
|
|
264
|
+
ERROR_CATEGORIES.USER_FILTERED,
|
|
265
|
+
ERROR_CATEGORIES.MISSING_DEPENDENCY
|
|
266
|
+
].includes(category);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Format error clarity block for CLI output
|
|
271
|
+
*
|
|
272
|
+
* @param {Array} failures - Failed attempts/flows
|
|
273
|
+
* @param {Object} config - Guardian config
|
|
274
|
+
* @param {Array} args - CLI arguments
|
|
275
|
+
* @returns {string} Formatted error clarity block
|
|
276
|
+
*/
|
|
277
|
+
function formatErrorClarity(failures = [], config = {}, args = []) {
|
|
278
|
+
if (!shouldShowErrorClarity(config, args)) {
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!failures || failures.length === 0) {
|
|
283
|
+
return '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const lines = [];
|
|
287
|
+
const groups = groupFailuresByCategory(failures);
|
|
288
|
+
|
|
289
|
+
// Separate actual errors from skips
|
|
290
|
+
const errors = {};
|
|
291
|
+
const skips = {};
|
|
292
|
+
|
|
293
|
+
Object.entries(groups).forEach(([category, categoryFailures]) => {
|
|
294
|
+
if (isSkip(category)) {
|
|
295
|
+
skips[category] = categoryFailures;
|
|
296
|
+
} else {
|
|
297
|
+
errors[category] = categoryFailures;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Print errors section
|
|
302
|
+
const errorCategories = Object.keys(errors);
|
|
303
|
+
if (errorCategories.length > 0) {
|
|
304
|
+
lines.push('');
|
|
305
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
306
|
+
lines.push('FAILURES & ERRORS');
|
|
307
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
308
|
+
lines.push('');
|
|
309
|
+
|
|
310
|
+
errorCategories.forEach(category => {
|
|
311
|
+
const categoryFailures = errors[category];
|
|
312
|
+
const errorInfo = getErrorInfo(categoryFailures[0]);
|
|
313
|
+
|
|
314
|
+
lines.push(`${errorInfo.title}`);
|
|
315
|
+
lines.push('────────────────────────────────────────────────────────────');
|
|
316
|
+
lines.push(`${errorInfo.explanation}`);
|
|
317
|
+
lines.push(`Action: ${errorInfo.action}`);
|
|
318
|
+
|
|
319
|
+
// List affected flows/attempts (max 3)
|
|
320
|
+
const deduped = deduplicateErrors(categoryFailures);
|
|
321
|
+
const names = deduped
|
|
322
|
+
.map(f => f.attemptName || f.name || f.attemptId || 'unknown');
|
|
323
|
+
|
|
324
|
+
if (names.length > 0) {
|
|
325
|
+
lines.push(`Affected: ${names.join(', ')}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (categoryFailures.length > 3) {
|
|
329
|
+
lines.push(`(+${categoryFailures.length - 3} more)`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
lines.push('');
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Print skips section
|
|
337
|
+
const skipCategories = Object.keys(skips);
|
|
338
|
+
if (skipCategories.length > 0) {
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
341
|
+
lines.push('SKIPPED ATTEMPTS');
|
|
342
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
343
|
+
lines.push('');
|
|
344
|
+
|
|
345
|
+
skipCategories.forEach(category => {
|
|
346
|
+
const categorySkips = skips[category];
|
|
347
|
+
const skipReason = SKIP_MESSAGES[category] || 'Skipped';
|
|
348
|
+
|
|
349
|
+
// Count skips in this category
|
|
350
|
+
const names = deduplicateErrors(categorySkips)
|
|
351
|
+
.map(s => s.attemptName || s.name || s.attemptId || 'unknown')
|
|
352
|
+
.slice(0, 3);
|
|
353
|
+
|
|
354
|
+
lines.push(`${skipReason} (${categorySkips.length})`);
|
|
355
|
+
lines.push('────────────────────────────────────────────────────────────');
|
|
356
|
+
if (names.length > 0) {
|
|
357
|
+
lines.push(`${names.join(', ')}`);
|
|
358
|
+
}
|
|
359
|
+
if (categorySkips.length > 3) {
|
|
360
|
+
lines.push(`+${categorySkips.length - 3} more`);
|
|
361
|
+
}
|
|
362
|
+
lines.push('');
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (lines.length > 1) {
|
|
367
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return lines.join('\n');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Print error clarity block to stdout
|
|
375
|
+
*
|
|
376
|
+
* @param {Array} failures - Failed attempts/flows
|
|
377
|
+
* @param {Object} config - Guardian config
|
|
378
|
+
* @param {Array} args - CLI arguments
|
|
379
|
+
*/
|
|
380
|
+
function printErrorClarity(failures = [], config = {}, args = []) {
|
|
381
|
+
const output = formatErrorClarity(failures, config, args);
|
|
382
|
+
if (output && output.trim().length > 0) {
|
|
383
|
+
console.log(output);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = {
|
|
388
|
+
ERROR_CATEGORIES,
|
|
389
|
+
ERROR_MESSAGES,
|
|
390
|
+
SKIP_MESSAGES,
|
|
391
|
+
classifyError,
|
|
392
|
+
getErrorInfo,
|
|
393
|
+
shouldShowErrorClarity,
|
|
394
|
+
groupFailuresByCategory,
|
|
395
|
+
deduplicateErrors,
|
|
396
|
+
isSkip,
|
|
397
|
+
formatErrorClarity,
|
|
398
|
+
printErrorClarity
|
|
399
|
+
};
|