@odavl/guardian 0.1.0-rc1
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 +20 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/bin/guardian.js +690 -0
- package/flows/example-login-flow.json +36 -0
- package/flows/example-signup-flow.json +44 -0
- package/guardian-contract-v1.md +149 -0
- package/guardian.config.json +54 -0
- package/guardian.policy.json +12 -0
- package/guardian.profile.docs.yaml +18 -0
- package/guardian.profile.ecommerce.yaml +17 -0
- package/guardian.profile.marketing.yaml +18 -0
- package/guardian.profile.saas.yaml +21 -0
- package/package.json +69 -0
- package/policies/enterprise.json +12 -0
- package/policies/saas.json +12 -0
- package/policies/startup.json +12 -0
- package/src/guardian/attempt-engine.js +454 -0
- package/src/guardian/attempt-registry.js +227 -0
- package/src/guardian/attempt-reporter.js +507 -0
- package/src/guardian/attempt.js +227 -0
- package/src/guardian/auto-attempt-builder.js +283 -0
- package/src/guardian/baseline-reporter.js +143 -0
- package/src/guardian/baseline-storage.js +285 -0
- package/src/guardian/baseline.js +492 -0
- package/src/guardian/behavioral-signals.js +261 -0
- package/src/guardian/breakage-intelligence.js +223 -0
- package/src/guardian/browser.js +92 -0
- package/src/guardian/cli-summary.js +141 -0
- package/src/guardian/crawler.js +142 -0
- package/src/guardian/discovery-engine.js +661 -0
- package/src/guardian/enhanced-html-reporter.js +305 -0
- package/src/guardian/failure-taxonomy.js +169 -0
- package/src/guardian/flow-executor.js +374 -0
- package/src/guardian/flow-registry.js +67 -0
- package/src/guardian/html-reporter.js +414 -0
- package/src/guardian/index.js +218 -0
- package/src/guardian/init-command.js +139 -0
- package/src/guardian/junit-reporter.js +264 -0
- package/src/guardian/market-criticality.js +335 -0
- package/src/guardian/market-reporter.js +305 -0
- package/src/guardian/network-trace.js +178 -0
- package/src/guardian/policy.js +357 -0
- package/src/guardian/preset-loader.js +148 -0
- package/src/guardian/reality.js +547 -0
- package/src/guardian/reporter.js +181 -0
- package/src/guardian/root-cause-analysis.js +171 -0
- package/src/guardian/safety.js +248 -0
- package/src/guardian/scan-presets.js +60 -0
- package/src/guardian/screenshot.js +152 -0
- package/src/guardian/sitemap.js +225 -0
- package/src/guardian/snapshot-schema.js +266 -0
- package/src/guardian/snapshot.js +327 -0
- package/src/guardian/validators.js +323 -0
- package/src/guardian/visual-diff.js +247 -0
- package/src/guardian/webhook.js +206 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guardian HTML Report Generator
|
|
3
|
+
* Creates beautiful, self-contained HTML reports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class GuardianHTMLReporter {
|
|
10
|
+
/**
|
|
11
|
+
* Generate HTML report from JSON report
|
|
12
|
+
* @param {object} jsonReport - JSON report object
|
|
13
|
+
* @param {string} artifactsDir - Directory containing artifacts
|
|
14
|
+
* @returns {string} HTML content
|
|
15
|
+
*/
|
|
16
|
+
generate(jsonReport, artifactsDir) {
|
|
17
|
+
const html = `<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="UTF-8">
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
22
|
+
<title>Guardian Report - ${jsonReport.baseUrl}</title>
|
|
23
|
+
<style>
|
|
24
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
25
|
+
body {
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
27
|
+
background: #f5f5f5;
|
|
28
|
+
color: #333;
|
|
29
|
+
line-height: 1.6;
|
|
30
|
+
}
|
|
31
|
+
.container {
|
|
32
|
+
max-width: 1200px;
|
|
33
|
+
margin: 0 auto;
|
|
34
|
+
padding: 20px;
|
|
35
|
+
}
|
|
36
|
+
.header {
|
|
37
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
38
|
+
color: white;
|
|
39
|
+
padding: 40px 20px;
|
|
40
|
+
border-radius: 10px;
|
|
41
|
+
margin-bottom: 30px;
|
|
42
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
43
|
+
}
|
|
44
|
+
.header h1 {
|
|
45
|
+
font-size: 2.5em;
|
|
46
|
+
margin-bottom: 10px;
|
|
47
|
+
}
|
|
48
|
+
.header .subtitle {
|
|
49
|
+
font-size: 1.2em;
|
|
50
|
+
opacity: 0.9;
|
|
51
|
+
}
|
|
52
|
+
.verdict {
|
|
53
|
+
background: white;
|
|
54
|
+
padding: 30px;
|
|
55
|
+
border-radius: 10px;
|
|
56
|
+
margin-bottom: 30px;
|
|
57
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
58
|
+
}
|
|
59
|
+
.verdict.ready {
|
|
60
|
+
border-left: 5px solid #10b981;
|
|
61
|
+
}
|
|
62
|
+
.verdict.do-not-launch {
|
|
63
|
+
border-left: 5px solid #ef4444;
|
|
64
|
+
}
|
|
65
|
+
.verdict.insufficient {
|
|
66
|
+
border-left: 5px solid #f59e0b;
|
|
67
|
+
}
|
|
68
|
+
.verdict-badge {
|
|
69
|
+
display: inline-block;
|
|
70
|
+
padding: 10px 20px;
|
|
71
|
+
border-radius: 20px;
|
|
72
|
+
font-weight: bold;
|
|
73
|
+
font-size: 1.2em;
|
|
74
|
+
margin-bottom: 15px;
|
|
75
|
+
}
|
|
76
|
+
.verdict-badge.ready {
|
|
77
|
+
background: #d1fae5;
|
|
78
|
+
color: #065f46;
|
|
79
|
+
}
|
|
80
|
+
.verdict-badge.do-not-launch {
|
|
81
|
+
background: #fee2e2;
|
|
82
|
+
color: #991b1b;
|
|
83
|
+
}
|
|
84
|
+
.verdict-badge.insufficient {
|
|
85
|
+
background: #fef3c7;
|
|
86
|
+
color: #92400e;
|
|
87
|
+
}
|
|
88
|
+
.metrics {
|
|
89
|
+
display: grid;
|
|
90
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
91
|
+
gap: 20px;
|
|
92
|
+
margin-bottom: 30px;
|
|
93
|
+
}
|
|
94
|
+
.metric-card {
|
|
95
|
+
background: white;
|
|
96
|
+
padding: 20px;
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
99
|
+
}
|
|
100
|
+
.metric-card .label {
|
|
101
|
+
font-size: 0.9em;
|
|
102
|
+
color: #666;
|
|
103
|
+
margin-bottom: 5px;
|
|
104
|
+
}
|
|
105
|
+
.metric-card .value {
|
|
106
|
+
font-size: 2em;
|
|
107
|
+
font-weight: bold;
|
|
108
|
+
color: #667eea;
|
|
109
|
+
}
|
|
110
|
+
.section {
|
|
111
|
+
background: white;
|
|
112
|
+
padding: 30px;
|
|
113
|
+
border-radius: 10px;
|
|
114
|
+
margin-bottom: 30px;
|
|
115
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
116
|
+
}
|
|
117
|
+
.section h2 {
|
|
118
|
+
margin-bottom: 20px;
|
|
119
|
+
color: #667eea;
|
|
120
|
+
border-bottom: 2px solid #f0f0f0;
|
|
121
|
+
padding-bottom: 10px;
|
|
122
|
+
}
|
|
123
|
+
.reason-list {
|
|
124
|
+
list-style: none;
|
|
125
|
+
}
|
|
126
|
+
.reason-list li {
|
|
127
|
+
padding: 10px;
|
|
128
|
+
margin-bottom: 10px;
|
|
129
|
+
background: #f9fafb;
|
|
130
|
+
border-left: 3px solid #667eea;
|
|
131
|
+
border-radius: 5px;
|
|
132
|
+
}
|
|
133
|
+
.screenshots {
|
|
134
|
+
display: grid;
|
|
135
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
136
|
+
gap: 20px;
|
|
137
|
+
}
|
|
138
|
+
.screenshot-card {
|
|
139
|
+
border: 1px solid #e5e7eb;
|
|
140
|
+
border-radius: 10px;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
transition: transform 0.2s;
|
|
143
|
+
}
|
|
144
|
+
.screenshot-card:hover {
|
|
145
|
+
transform: translateY(-5px);
|
|
146
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
|
147
|
+
}
|
|
148
|
+
.screenshot-card img {
|
|
149
|
+
width: 100%;
|
|
150
|
+
height: 200px;
|
|
151
|
+
object-fit: cover;
|
|
152
|
+
}
|
|
153
|
+
.screenshot-card .caption {
|
|
154
|
+
padding: 10px;
|
|
155
|
+
background: #f9fafb;
|
|
156
|
+
font-size: 0.9em;
|
|
157
|
+
color: #666;
|
|
158
|
+
}
|
|
159
|
+
.page-table {
|
|
160
|
+
width: 100%;
|
|
161
|
+
border-collapse: collapse;
|
|
162
|
+
}
|
|
163
|
+
.page-table th {
|
|
164
|
+
background: #f9fafb;
|
|
165
|
+
padding: 12px;
|
|
166
|
+
text-align: left;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
border-bottom: 2px solid #e5e7eb;
|
|
169
|
+
}
|
|
170
|
+
.page-table td {
|
|
171
|
+
padding: 12px;
|
|
172
|
+
border-bottom: 1px solid #e5e7eb;
|
|
173
|
+
}
|
|
174
|
+
.page-table tr:hover {
|
|
175
|
+
background: #f9fafb;
|
|
176
|
+
}
|
|
177
|
+
.status-badge {
|
|
178
|
+
display: inline-block;
|
|
179
|
+
padding: 4px 12px;
|
|
180
|
+
border-radius: 12px;
|
|
181
|
+
font-size: 0.85em;
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
}
|
|
184
|
+
.status-success {
|
|
185
|
+
background: #d1fae5;
|
|
186
|
+
color: #065f46;
|
|
187
|
+
}
|
|
188
|
+
.status-error {
|
|
189
|
+
background: #fee2e2;
|
|
190
|
+
color: #991b1b;
|
|
191
|
+
}
|
|
192
|
+
.footer {
|
|
193
|
+
text-align: center;
|
|
194
|
+
padding: 20px;
|
|
195
|
+
color: #666;
|
|
196
|
+
font-size: 0.9em;
|
|
197
|
+
}
|
|
198
|
+
.confidence-high { color: #10b981; font-weight: bold; }
|
|
199
|
+
.confidence-medium { color: #f59e0b; font-weight: bold; }
|
|
200
|
+
.confidence-low { color: #ef4444; font-weight: bold; }
|
|
201
|
+
</style>
|
|
202
|
+
</head>
|
|
203
|
+
<body>
|
|
204
|
+
<div class="container">
|
|
205
|
+
<div class="header">
|
|
206
|
+
<h1>š”ļø ODAVL Guardian</h1>
|
|
207
|
+
<div class="subtitle">Market Reality Testing Report</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
${this.generateVerdictSection(jsonReport)}
|
|
211
|
+
${this.generateMetricsSection(jsonReport)}
|
|
212
|
+
${this.generateReasonsSection(jsonReport)}
|
|
213
|
+
${this.generatePagesSection(jsonReport)}
|
|
214
|
+
${this.generateScreenshotsSection(jsonReport, artifactsDir)}
|
|
215
|
+
|
|
216
|
+
<div class="footer">
|
|
217
|
+
Generated by ODAVL Guardian ⢠${new Date(jsonReport.timestamp).toLocaleString()}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</body>
|
|
221
|
+
</html>`;
|
|
222
|
+
|
|
223
|
+
return html;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate verdict section
|
|
228
|
+
*/
|
|
229
|
+
generateVerdictSection(report) {
|
|
230
|
+
const decision = report.finalJudgment.decision;
|
|
231
|
+
const decisionClass = decision.toLowerCase().replace(/_/g, '-');
|
|
232
|
+
const decisionIcon = decision === 'READY' ? 'š¢' : decision === 'DO_NOT_LAUNCH' ? 'š“' : 'š”';
|
|
233
|
+
const decisionText = decision === 'READY' ? 'Safe to Launch' : decision === 'DO_NOT_LAUNCH' ? 'DO NOT LAUNCH' : 'Insufficient Confidence';
|
|
234
|
+
|
|
235
|
+
return `
|
|
236
|
+
<div class="verdict ${decisionClass}">
|
|
237
|
+
<div class="verdict-badge ${decisionClass}">
|
|
238
|
+
${decisionIcon} ${decisionText}
|
|
239
|
+
</div>
|
|
240
|
+
<p style="margin-top: 15px; font-size: 1.1em;">
|
|
241
|
+
<strong>Target:</strong> ${report.baseUrl}
|
|
242
|
+
</p>
|
|
243
|
+
<p style="margin-top: 10px;">
|
|
244
|
+
<strong>Confidence:</strong>
|
|
245
|
+
<span class="confidence-${report.confidence.level.toLowerCase()}">${report.confidence.level}</span>
|
|
246
|
+
</p>
|
|
247
|
+
<p style="margin-top: 5px; color: #666;">
|
|
248
|
+
${report.confidence.reasoning}
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Generate metrics section
|
|
256
|
+
*/
|
|
257
|
+
generateMetricsSection(report) {
|
|
258
|
+
return `
|
|
259
|
+
<div class="metrics">
|
|
260
|
+
<div class="metric-card">
|
|
261
|
+
<div class="label">Coverage</div>
|
|
262
|
+
<div class="value">${report.summary.coverage}%</div>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="metric-card">
|
|
265
|
+
<div class="label">Pages Visited</div>
|
|
266
|
+
<div class="value">${report.summary.visitedPages}</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="metric-card">
|
|
269
|
+
<div class="label">Pages Discovered</div>
|
|
270
|
+
<div class="value">${report.summary.discoveredPages}</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="metric-card">
|
|
273
|
+
<div class="label">Failed Pages</div>
|
|
274
|
+
<div class="value" style="color: ${report.summary.failedPages > 0 ? '#ef4444' : '#10b981'}">
|
|
275
|
+
${report.summary.failedPages}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Generate reasons section
|
|
284
|
+
*/
|
|
285
|
+
generateReasonsSection(report) {
|
|
286
|
+
const reasons = report.finalJudgment.reasons || [];
|
|
287
|
+
if (reasons.length === 0) return '';
|
|
288
|
+
|
|
289
|
+
const reasonItems = reasons.map(r => `<li>${r}</li>`).join('');
|
|
290
|
+
|
|
291
|
+
return `
|
|
292
|
+
<div class="section">
|
|
293
|
+
<h2>š Decision Reasons</h2>
|
|
294
|
+
<ul class="reason-list">
|
|
295
|
+
${reasonItems}
|
|
296
|
+
</ul>
|
|
297
|
+
</div>
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Generate pages section
|
|
303
|
+
*/
|
|
304
|
+
generatePagesSection(report) {
|
|
305
|
+
const pages = report.pages || [];
|
|
306
|
+
if (pages.length === 0) return '';
|
|
307
|
+
|
|
308
|
+
const rows = pages.map(page => {
|
|
309
|
+
const statusClass = page.status >= 200 && page.status < 400 ? 'status-success' : 'status-error';
|
|
310
|
+
const statusText = page.status || 'N/A';
|
|
311
|
+
|
|
312
|
+
return `
|
|
313
|
+
<tr>
|
|
314
|
+
<td>${page.index}</td>
|
|
315
|
+
<td style="word-break: break-all;">${page.url}</td>
|
|
316
|
+
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
|
317
|
+
<td>${page.links || 0}</td>
|
|
318
|
+
</tr>
|
|
319
|
+
`;
|
|
320
|
+
}).join('');
|
|
321
|
+
|
|
322
|
+
return `
|
|
323
|
+
<div class="section">
|
|
324
|
+
<h2>š Pages Visited</h2>
|
|
325
|
+
<table class="page-table">
|
|
326
|
+
<thead>
|
|
327
|
+
<tr>
|
|
328
|
+
<th>#</th>
|
|
329
|
+
<th>URL</th>
|
|
330
|
+
<th>Status</th>
|
|
331
|
+
<th>Links</th>
|
|
332
|
+
</tr>
|
|
333
|
+
</thead>
|
|
334
|
+
<tbody>
|
|
335
|
+
${rows}
|
|
336
|
+
</tbody>
|
|
337
|
+
</table>
|
|
338
|
+
</div>
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Generate screenshots section
|
|
344
|
+
*/
|
|
345
|
+
generateScreenshotsSection(report, artifactsDir) {
|
|
346
|
+
const pagesDir = path.join(artifactsDir, 'pages');
|
|
347
|
+
|
|
348
|
+
// Check if pages directory exists
|
|
349
|
+
if (!fs.existsSync(pagesDir)) {
|
|
350
|
+
return '';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get all screenshot files
|
|
354
|
+
const files = fs.readdirSync(pagesDir).filter(f => f.endsWith('.jpeg') || f.endsWith('.jpg') || f.endsWith('.png'));
|
|
355
|
+
|
|
356
|
+
if (files.length === 0) {
|
|
357
|
+
return '';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const cards = files.map(file => {
|
|
361
|
+
const relativePath = `pages/${file}`;
|
|
362
|
+
return `
|
|
363
|
+
<div class="screenshot-card">
|
|
364
|
+
<img src="${relativePath}" alt="${file}" loading="lazy">
|
|
365
|
+
<div class="caption">${file}</div>
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
}).join('');
|
|
369
|
+
|
|
370
|
+
return `
|
|
371
|
+
<div class="section">
|
|
372
|
+
<h2>šø Screenshots</h2>
|
|
373
|
+
<div class="screenshots">
|
|
374
|
+
${cards}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Save HTML report to file
|
|
382
|
+
* @param {string} html - HTML content
|
|
383
|
+
* @param {string} outputPath - Where to save the HTML file
|
|
384
|
+
* @returns {boolean} Success status
|
|
385
|
+
*/
|
|
386
|
+
save(html, outputPath) {
|
|
387
|
+
try {
|
|
388
|
+
fs.writeFileSync(outputPath, html, 'utf8');
|
|
389
|
+
return true;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error(`ā Failed to save HTML report: ${error.message}`);
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Generate and save HTML report
|
|
398
|
+
* @param {object} jsonReport - JSON report object
|
|
399
|
+
* @param {string} artifactsDir - Directory containing artifacts
|
|
400
|
+
* @returns {boolean} Success status
|
|
401
|
+
*/
|
|
402
|
+
generateAndSave(jsonReport, artifactsDir) {
|
|
403
|
+
try {
|
|
404
|
+
const html = this.generate(jsonReport, artifactsDir);
|
|
405
|
+
const outputPath = path.join(artifactsDir, 'report.html');
|
|
406
|
+
return this.save(html, outputPath);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error(`ā Failed to generate HTML report: ${error.message}`);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = GuardianHTMLReporter;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const { GuardianBrowser } = require('./browser');
|
|
2
|
+
const { GuardianCrawler } = require('./crawler');
|
|
3
|
+
const { GuardianReporter } = require('./reporter');
|
|
4
|
+
const GuardianScreenshot = require('./screenshot');
|
|
5
|
+
const GuardianNetworkTrace = require('./network-trace');
|
|
6
|
+
const GuardianSitemap = require('./sitemap');
|
|
7
|
+
const GuardianSafety = require('./safety');
|
|
8
|
+
const GuardianFlowExecutor = require('./flow-executor');
|
|
9
|
+
const GuardianHTMLReporter = require('./html-reporter');
|
|
10
|
+
|
|
11
|
+
async function runGuardian(config) {
|
|
12
|
+
const {
|
|
13
|
+
baseUrl,
|
|
14
|
+
maxPages = 25,
|
|
15
|
+
maxDepth = 3,
|
|
16
|
+
timeout = 20000,
|
|
17
|
+
artifactsDir = './artifacts',
|
|
18
|
+
// Phase 2 features
|
|
19
|
+
enableScreenshots = true,
|
|
20
|
+
enableTrace = true,
|
|
21
|
+
enableHAR = true,
|
|
22
|
+
enableSitemap = true,
|
|
23
|
+
enableSafety = true,
|
|
24
|
+
enableHTMLReport = true,
|
|
25
|
+
flowPath = null, // Path to flow JSON file
|
|
26
|
+
} = config;
|
|
27
|
+
|
|
28
|
+
// Validate baseUrl
|
|
29
|
+
try {
|
|
30
|
+
new URL(baseUrl);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(`ā Invalid URL: ${baseUrl}`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\nš”ļø ODAVL Guardian ā Market Reality Testing Engine`);
|
|
37
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
38
|
+
console.log(`š Target: ${baseUrl}`);
|
|
39
|
+
console.log(`āļø Config: max-pages=${maxPages}, max-depth=${maxDepth}, timeout=${timeout}ms`);
|
|
40
|
+
|
|
41
|
+
// Initialize modules
|
|
42
|
+
const screenshot = enableScreenshots ? new GuardianScreenshot() : null;
|
|
43
|
+
const networkTrace = (enableHAR || enableTrace) ? new GuardianNetworkTrace({ enableHAR, enableTrace }) : null;
|
|
44
|
+
const sitemap = enableSitemap ? new GuardianSitemap() : null;
|
|
45
|
+
const safety = enableSafety ? new GuardianSafety() : null;
|
|
46
|
+
const flowExecutor = flowPath ? new GuardianFlowExecutor({ safety, screenshotOnStep: enableScreenshots }) : null;
|
|
47
|
+
const htmlReporter = enableHTMLReport ? new GuardianHTMLReporter() : null;
|
|
48
|
+
|
|
49
|
+
const browser = new GuardianBrowser();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Discover URLs from sitemap (if enabled)
|
|
53
|
+
let sitemapUrls = [];
|
|
54
|
+
if (sitemap) {
|
|
55
|
+
const sitemapResult = await sitemap.discover(baseUrl);
|
|
56
|
+
if (sitemapResult.urls.length > 0) {
|
|
57
|
+
sitemapUrls = sitemap.filterSameOrigin(sitemapResult.urls, baseUrl);
|
|
58
|
+
console.log(`šŗļø Sitemap: Discovered ${sitemapUrls.length} URLs`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Launch browser
|
|
63
|
+
console.log(`\nš Launching browser...`);
|
|
64
|
+
const launchOptions = {};
|
|
65
|
+
|
|
66
|
+
// Enable HAR if requested
|
|
67
|
+
if (networkTrace && enableHAR) {
|
|
68
|
+
// HAR must be configured before context creation
|
|
69
|
+
launchOptions.recordHar = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await browser.launch(timeout, launchOptions);
|
|
73
|
+
console.log(`ā
Browser launched`);
|
|
74
|
+
|
|
75
|
+
// Start trace recording if enabled
|
|
76
|
+
let tracePath = null;
|
|
77
|
+
if (networkTrace && enableTrace && browser.context) {
|
|
78
|
+
const reporter = new GuardianReporter();
|
|
79
|
+
const { runDir } = reporter.prepareArtifactsDir(artifactsDir);
|
|
80
|
+
tracePath = await networkTrace.startTrace(browser.context, runDir);
|
|
81
|
+
if (tracePath) {
|
|
82
|
+
console.log(`š¹ Trace recording started`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Flow execution OR crawling
|
|
87
|
+
let crawlResult = null;
|
|
88
|
+
let flowResult = null;
|
|
89
|
+
|
|
90
|
+
if (flowExecutor && flowPath) {
|
|
91
|
+
// Execute flow instead of crawling
|
|
92
|
+
console.log(`\nš¬ Flow execution mode`);
|
|
93
|
+
const flow = flowExecutor.loadFlow(flowPath);
|
|
94
|
+
|
|
95
|
+
if (!flow) {
|
|
96
|
+
throw new Error(`Failed to load flow from: ${flowPath}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const validation = flowExecutor.validateFlow(flow);
|
|
100
|
+
if (!validation.valid) {
|
|
101
|
+
throw new Error(`Invalid flow: ${validation.errors.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reporter = new GuardianReporter();
|
|
105
|
+
const { runDir } = reporter.prepareArtifactsDir(artifactsDir);
|
|
106
|
+
|
|
107
|
+
flowResult = await flowExecutor.executeFlow(browser.page, flow, runDir);
|
|
108
|
+
|
|
109
|
+
if (!flowResult.success) {
|
|
110
|
+
console.log(`ā Flow failed at step ${flowResult.failedStep}: ${flowResult.error}`);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Normal crawling mode
|
|
114
|
+
console.log(`\nš Starting crawl...`);
|
|
115
|
+
const crawler = new GuardianCrawler(baseUrl, maxPages, maxDepth);
|
|
116
|
+
|
|
117
|
+
// Add sitemap URLs to crawler if available
|
|
118
|
+
if (sitemapUrls.length > 0) {
|
|
119
|
+
crawler.discovered = new Set([...crawler.discovered, ...sitemapUrls]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add safety guard to crawler
|
|
123
|
+
if (safety) {
|
|
124
|
+
crawler.safety = safety;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add screenshot capability
|
|
128
|
+
if (screenshot) {
|
|
129
|
+
crawler.screenshot = screenshot;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Prepare artifacts directory
|
|
133
|
+
const reporter = new GuardianReporter();
|
|
134
|
+
const { runDir } = reporter.prepareArtifactsDir(artifactsDir);
|
|
135
|
+
|
|
136
|
+
crawlResult = await crawler.crawl(browser, runDir);
|
|
137
|
+
|
|
138
|
+
console.log(`ā
Crawl complete: visited ${crawlResult.totalVisited}/${crawlResult.totalDiscovered} pages`);
|
|
139
|
+
|
|
140
|
+
if (safety && crawlResult.safetyStats) {
|
|
141
|
+
const blocked = crawlResult.safetyStats.urlsBlocked || 0;
|
|
142
|
+
if (blocked > 0) {
|
|
143
|
+
console.log(`š”ļø Safety: Blocked ${blocked} dangerous URLs`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Stop trace recording
|
|
149
|
+
if (networkTrace && enableTrace && tracePath && browser.context) {
|
|
150
|
+
await networkTrace.stopTrace(browser.context, tracePath);
|
|
151
|
+
console.log(`ā
Trace saved: trace.zip`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Generate report
|
|
155
|
+
console.log(`\nš Generating report...`);
|
|
156
|
+
const reporter = new GuardianReporter();
|
|
157
|
+
|
|
158
|
+
let report;
|
|
159
|
+
if (flowResult) {
|
|
160
|
+
// Create report from flow execution
|
|
161
|
+
report = reporter.createFlowReport(flowResult, baseUrl);
|
|
162
|
+
} else {
|
|
163
|
+
// Create report from crawl
|
|
164
|
+
report = reporter.createReport(crawlResult, baseUrl);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Save JSON report
|
|
168
|
+
const savedReport = reporter.saveReport(report, artifactsDir);
|
|
169
|
+
console.log(`ā
Report saved to: ${savedReport.runDir}`);
|
|
170
|
+
|
|
171
|
+
// Generate HTML report if enabled
|
|
172
|
+
if (htmlReporter) {
|
|
173
|
+
const htmlSaved = htmlReporter.generateAndSave(report, savedReport.runDir);
|
|
174
|
+
if (htmlSaved) {
|
|
175
|
+
console.log(`ā
HTML report: report.html`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Display verdict
|
|
180
|
+
console.log(`\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
181
|
+
|
|
182
|
+
const { decision } = report.finalJudgment;
|
|
183
|
+
const coverageStr = `${report.summary.coverage}%`;
|
|
184
|
+
|
|
185
|
+
if (decision === 'READY') {
|
|
186
|
+
console.log(`\nš¢ READY ā Safe to launch`);
|
|
187
|
+
} else if (decision === 'DO_NOT_LAUNCH') {
|
|
188
|
+
console.log(`\nš“ DO_NOT_LAUNCH ā Issues found`);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(`\nš” INSUFFICIENT_CONFIDENCE ā Needs more data`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(`\nš Coverage: ${coverageStr}`);
|
|
194
|
+
console.log(`š Pages visited: ${report.summary.visitedPages}`);
|
|
195
|
+
console.log(`ā Failed pages: ${report.summary.failedPages}`);
|
|
196
|
+
console.log(`š¬ Confidence: ${report.confidence.level}`);
|
|
197
|
+
|
|
198
|
+
console.log(`\nš Reasons:`);
|
|
199
|
+
report.finalJudgment.reasons.forEach(reason => {
|
|
200
|
+
console.log(` ⢠${reason}`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
console.log(`\nš¾ Full report: ${savedReport.reportPath}`);
|
|
204
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n`);
|
|
205
|
+
|
|
206
|
+
// Exit with appropriate code
|
|
207
|
+
const exitCode = (decision === 'READY') ? 0 : 1;
|
|
208
|
+
process.exit(exitCode);
|
|
209
|
+
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(`\nā Error: ${err.message}`);
|
|
212
|
+
process.exit(2);
|
|
213
|
+
} finally {
|
|
214
|
+
await browser.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { runGuardian };
|