argusqa-os 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +879 -0
- package/package.json +69 -0
- package/src/adapters/browser.js +82 -0
- package/src/argus.js +8 -0
- package/src/batch-runner.js +8 -0
- package/src/cli/init.js +314 -0
- package/src/config/schema.js +108 -0
- package/src/config/targets.js +309 -0
- package/src/domain/finding.js +25 -0
- package/src/mcp-server.js +156 -0
- package/src/orchestration/crawl-and-report.js +16 -0
- package/src/orchestration/dispatcher.js +263 -0
- package/src/orchestration/env-comparison.js +498 -0
- package/src/orchestration/orchestrator.js +1128 -0
- package/src/orchestration/report-processor.js +134 -0
- package/src/orchestration/slack-notifier.js +337 -0
- package/src/orchestration/watch-mode.js +316 -0
- package/src/registry.js +18 -0
- package/src/server/index.js +94 -0
- package/src/server/interaction-handler.js +126 -0
- package/src/server/slash-command-handler.js +185 -0
- package/src/utils/api-frequency.js +128 -0
- package/src/utils/baseline-manager.js +255 -0
- package/src/utils/codebase-analyzer.js +299 -0
- package/src/utils/content-analyzer.js +155 -0
- package/src/utils/contract-validator.js +178 -0
- package/src/utils/css-analyzer.js +407 -0
- package/src/utils/diff.js +189 -0
- package/src/utils/flakiness-detector.js +82 -0
- package/src/utils/flow-runner.js +572 -0
- package/src/utils/github-reporter.js +310 -0
- package/src/utils/hover-analyzer.js +214 -0
- package/src/utils/html-reporter.js +301 -0
- package/src/utils/issues-analyzer.js +171 -0
- package/src/utils/keyboard-analyzer.js +141 -0
- package/src/utils/lighthouse-checker.js +120 -0
- package/src/utils/logger.js +39 -0
- package/src/utils/login-orchestrator.js +99 -0
- package/src/utils/mcp-client.js +264 -0
- package/src/utils/mcp-parsers.js +57 -0
- package/src/utils/memory-analyzer.js +270 -0
- package/src/utils/network-timing-analyzer.js +76 -0
- package/src/utils/parallel-crawler.js +28 -0
- package/src/utils/responsive-analyzer.js +253 -0
- package/src/utils/retry.js +36 -0
- package/src/utils/route-discoverer.js +306 -0
- package/src/utils/security-analyzer.js +302 -0
- package/src/utils/seo-analyzer.js +164 -0
- package/src/utils/session-manager.js +12 -0
- package/src/utils/session-persistence.js +214 -0
- package/src/utils/severity-overrides.js +91 -0
- package/src/utils/slack-guard.js +18 -0
- package/src/utils/slug.js +8 -0
- package/src/utils/snapshot-analyzer.js +330 -0
- package/src/utils/telemetry.js +190 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argus Report Dispatcher (v9.3.0)
|
|
3
|
+
*
|
|
4
|
+
* Dispatches a completed report to Slack, GitHub, and/or HTML.
|
|
5
|
+
* Extracted from crawl-and-report.js god object.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { execFile } from 'child_process';
|
|
10
|
+
import { childLogger } from '../utils/logger.js';
|
|
11
|
+
import { startSpan } from '../utils/telemetry.js';
|
|
12
|
+
import { postBugReport } from './slack-notifier.js';
|
|
13
|
+
import { isSlackConfigured } from '../utils/slack-guard.js';
|
|
14
|
+
import { isGitHubConfigured, reportToGitHub } from '../utils/github-reporter.js';
|
|
15
|
+
import { generateHtmlReport } from '../utils/html-reporter.js';
|
|
16
|
+
|
|
17
|
+
const logger = childLogger('dispatcher');
|
|
18
|
+
|
|
19
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Open a local file in the OS default browser (best-effort, skipped in CI).
|
|
23
|
+
*/
|
|
24
|
+
function openInBrowser(filePath) {
|
|
25
|
+
if (process.env.CI) return;
|
|
26
|
+
try {
|
|
27
|
+
const abs = path.resolve(filePath);
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
execFile('cmd', ['/c', 'start', '', abs], () => {});
|
|
30
|
+
} else if (process.platform === 'darwin') {
|
|
31
|
+
execFile('open', [abs], () => {});
|
|
32
|
+
} else {
|
|
33
|
+
execFile('xdg-open', [abs], () => {});
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// no display available — skip silently
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Safely extract the display message from any finding object.
|
|
42
|
+
*/
|
|
43
|
+
function errorText(e) {
|
|
44
|
+
return e.message
|
|
45
|
+
?? e.description
|
|
46
|
+
?? (e.requestUrl ? `HTTP ${e.status ?? '?'} — ${e.method ?? 'GET'} ${e.requestUrl}` : null)
|
|
47
|
+
?? `${e.type ?? 'unknown error'}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Slack Dispatch ────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Send Slack notifications for bugs found in the report.
|
|
54
|
+
*
|
|
55
|
+
* Criticals → one message per route with screenshot attached
|
|
56
|
+
* Warnings → one message per route (grouped)
|
|
57
|
+
* Info → single digest message summarising all routes
|
|
58
|
+
*/
|
|
59
|
+
async function dispatchToSlack(report, diff) {
|
|
60
|
+
const { summary } = report;
|
|
61
|
+
|
|
62
|
+
// ── Criticals: one message per affected route ─────────────────────────────
|
|
63
|
+
for (const routeResult of report.routes) {
|
|
64
|
+
const criticals = routeResult.errors.filter(e => e.severity === 'critical' && e.isNew === true);
|
|
65
|
+
if (criticals.length === 0) continue;
|
|
66
|
+
|
|
67
|
+
const description = criticals
|
|
68
|
+
.map(e => `• *[${e.type}]* ${errorText(e)}`)
|
|
69
|
+
.join('\n');
|
|
70
|
+
|
|
71
|
+
await postBugReport({
|
|
72
|
+
severity: 'critical',
|
|
73
|
+
title: `${criticals.length} critical issue(s) on ${routeResult.route}`,
|
|
74
|
+
description,
|
|
75
|
+
url: routeResult.url,
|
|
76
|
+
screenshotPath: routeResult.screenshot,
|
|
77
|
+
details: { route: routeResult.route, errors: criticals },
|
|
78
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: critical report failed for ${routeResult.route}: ${err.message}`));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Warnings: one message per affected route ──────────────────────────────
|
|
82
|
+
for (const routeResult of report.routes) {
|
|
83
|
+
const warnings = routeResult.errors.filter(e => e.severity === 'warning' && e.isNew === true);
|
|
84
|
+
if (warnings.length === 0) continue;
|
|
85
|
+
|
|
86
|
+
const description = warnings
|
|
87
|
+
.map(e => `• *[${e.type}]* ${errorText(e)}`)
|
|
88
|
+
.join('\n');
|
|
89
|
+
|
|
90
|
+
await postBugReport({
|
|
91
|
+
severity: 'warning',
|
|
92
|
+
title: `${warnings.length} warning(s) on ${routeResult.route}`,
|
|
93
|
+
description,
|
|
94
|
+
url: routeResult.url,
|
|
95
|
+
screenshotPath: routeResult.screenshot,
|
|
96
|
+
details: { route: routeResult.route, errors: warnings },
|
|
97
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: warning report failed for ${routeResult.route}: ${err.message}`));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Responsive screenshots: mobile view for routes with responsive findings
|
|
101
|
+
for (const routeResult of report.routes) {
|
|
102
|
+
const responsiveErrors = routeResult.errors.filter(e =>
|
|
103
|
+
e.type === 'responsive_overflow' || e.type === 'responsive_small_touch_target'
|
|
104
|
+
);
|
|
105
|
+
const mobileShot = routeResult.responsiveScreenshots?.['375x812'];
|
|
106
|
+
if (responsiveErrors.length === 0 || !mobileShot) continue;
|
|
107
|
+
|
|
108
|
+
const description = responsiveErrors.map(e => `• *[${e.type}]* ${errorText(e)}`).join('\n');
|
|
109
|
+
await postBugReport({
|
|
110
|
+
severity: 'warning',
|
|
111
|
+
title: `Responsive layout issues — ${routeResult.route} (mobile screenshot)`,
|
|
112
|
+
description: `${description}\n\n_375px mobile view attached. Full grid: ${
|
|
113
|
+
Object.keys(routeResult.responsiveScreenshots ?? {}).join(', ')
|
|
114
|
+
}_`,
|
|
115
|
+
url: routeResult.url,
|
|
116
|
+
screenshotPath: mobileShot,
|
|
117
|
+
details: { responsiveFindings: responsiveErrors },
|
|
118
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: responsive report failed for ${routeResult.route}: ${err.message}`));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Flow failures ─────────────────────────────────────────────────────────
|
|
122
|
+
for (const flowResult of (report.flows ?? [])) {
|
|
123
|
+
const flowCriticals = (flowResult.findings ?? []).filter(f => f.severity === 'critical' && f.isNew === true);
|
|
124
|
+
if (flowCriticals.length > 0) {
|
|
125
|
+
await postBugReport({
|
|
126
|
+
severity: 'critical',
|
|
127
|
+
title: `Flow "${flowResult.flowName}" failed — ${flowCriticals.length} critical issue(s)`,
|
|
128
|
+
description: flowCriticals.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
|
|
129
|
+
url: report.baseUrl,
|
|
130
|
+
screenshotPath: null,
|
|
131
|
+
details: { flow: flowResult.flowName, errors: flowCriticals },
|
|
132
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: flow critical report failed for ${flowResult.flowName}: ${err.message}`));
|
|
133
|
+
}
|
|
134
|
+
const flowWarnings = (flowResult.findings ?? []).filter(f => f.severity === 'warning' && f.isNew === true);
|
|
135
|
+
if (flowWarnings.length > 0) {
|
|
136
|
+
await postBugReport({
|
|
137
|
+
severity: 'warning',
|
|
138
|
+
title: `Flow "${flowResult.flowName}" — ${flowWarnings.length} warning(s)`,
|
|
139
|
+
description: flowWarnings.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
|
|
140
|
+
url: report.baseUrl,
|
|
141
|
+
screenshotPath: null,
|
|
142
|
+
details: { flow: flowResult.flowName, errors: flowWarnings },
|
|
143
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: flow warning report failed for ${flowResult.flowName}: ${err.message}`));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Codebase criticals + warnings ─────────────────────────────────────────
|
|
148
|
+
const cbCriticals = (report.codebase ?? []).filter(f => f.severity === 'critical' && f.isNew === true);
|
|
149
|
+
if (cbCriticals.length > 0) {
|
|
150
|
+
await postBugReport({
|
|
151
|
+
severity: 'critical',
|
|
152
|
+
title: `${cbCriticals.length} codebase critical(s) — ${report.baseUrl}`,
|
|
153
|
+
description: cbCriticals.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
|
|
154
|
+
url: report.baseUrl,
|
|
155
|
+
screenshotPath: null,
|
|
156
|
+
details: { codebase: cbCriticals },
|
|
157
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: codebase critical report failed: ${err.message}`));
|
|
158
|
+
}
|
|
159
|
+
const cbWarnings = (report.codebase ?? []).filter(f => f.severity === 'warning' && f.isNew === true);
|
|
160
|
+
if (cbWarnings.length > 0) {
|
|
161
|
+
await postBugReport({
|
|
162
|
+
severity: 'warning',
|
|
163
|
+
title: `${cbWarnings.length} codebase warning(s) — ${report.baseUrl}`,
|
|
164
|
+
description: cbWarnings.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
|
|
165
|
+
url: report.baseUrl,
|
|
166
|
+
screenshotPath: null,
|
|
167
|
+
details: { codebase: cbWarnings },
|
|
168
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: codebase warning report failed: ${err.message}`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Info digest: one summary message across all routes ────────────────────
|
|
172
|
+
const allInfos = report.routes.flatMap(r =>
|
|
173
|
+
r.errors.filter(e => e.severity === 'info' && e.isNew !== false).map(e => ({ ...e, routeName: r.route }))
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const digestLines = [];
|
|
177
|
+
for (const routeResult of report.routes) {
|
|
178
|
+
const routeInfos = allInfos.filter(e => e.routeName === routeResult.route);
|
|
179
|
+
if (routeInfos.length === 0) continue;
|
|
180
|
+
digestLines.push(`*${routeResult.route}* (${routeResult.url})`);
|
|
181
|
+
for (const e of routeInfos) {
|
|
182
|
+
const flakyTag = e.flaky ? ' :zap: _flaky_' : '';
|
|
183
|
+
digestLines.push(` • [${e.type}]${flakyTag} ${errorText(e)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const flowResult of (report.flows ?? [])) {
|
|
188
|
+
const flowInfos = (flowResult.findings ?? []).filter(e => e.severity === 'info');
|
|
189
|
+
if (flowInfos.length === 0) continue;
|
|
190
|
+
digestLines.push(`*Flow: ${flowResult.flowName}* (${flowResult.stepsCompleted}/${flowResult.totalSteps} steps — ${flowResult.status})`);
|
|
191
|
+
for (const e of flowInfos) {
|
|
192
|
+
digestLines.push(` • [${e.type}] ${errorText(e)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const cbInfos = (report.codebase ?? []).filter(f => f.severity === 'info');
|
|
197
|
+
if (cbInfos.length > 0) {
|
|
198
|
+
digestLines.push('*Codebase (C1)*');
|
|
199
|
+
for (const f of cbInfos) digestLines.push(` • [${f.type}] ${errorText(f)}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const allFlowInfos = (report.flows ?? []).flatMap(f => (f.findings ?? []).filter(e => e.severity === 'info'));
|
|
203
|
+
|
|
204
|
+
if (allInfos.length > 0 || allFlowInfos.length > 0 || cbInfos.length > 0) {
|
|
205
|
+
const runDate = new Date(report.generatedAt).toLocaleString();
|
|
206
|
+
const trendLine = diff
|
|
207
|
+
? diff.isFirstRun
|
|
208
|
+
? '_Baseline established — future runs will show new / resolved counts._'
|
|
209
|
+
: `:chart_with_upwards_trend: ${diff.newCount} new :white_check_mark: ${diff.resolvedCount} resolved since last baseline` +
|
|
210
|
+
((diff.flowNewCount ?? 0) > 0 || (diff.flowResolvedCount ?? 0) > 0
|
|
211
|
+
? ` _(flows: ${diff.flowNewCount ?? 0} new, ${diff.flowResolvedCount ?? 0} resolved)_`
|
|
212
|
+
: '')
|
|
213
|
+
: '';
|
|
214
|
+
|
|
215
|
+
await postBugReport({
|
|
216
|
+
severity: 'info',
|
|
217
|
+
title: `Argus crawl digest — ${report.baseUrl} (${runDate})`,
|
|
218
|
+
description:
|
|
219
|
+
`Summary: ${summary.total} findings across ${report.routes.length} routes\n` +
|
|
220
|
+
`:red_circle: ${summary.critical} critical :large_yellow_circle: ${summary.warning} warnings :large_blue_circle: ${summary.info} info\n` +
|
|
221
|
+
(trendLine ? trendLine + '\n' : '') + '\n' +
|
|
222
|
+
(digestLines.length > 0 ? digestLines.join('\n') : '_No info-level findings._'),
|
|
223
|
+
url: report.baseUrl,
|
|
224
|
+
screenshotPath: null,
|
|
225
|
+
details: { summary, infos: allInfos },
|
|
226
|
+
}).catch(err => logger.warn(`[ARGUS] Slack: info digest report failed: ${err.message}`));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Dispatch the completed report to all configured channels (Slack + GitHub + HTML).
|
|
234
|
+
* Each channel is best-effort — a failure in one does not block the others.
|
|
235
|
+
*
|
|
236
|
+
* @param {object} report - Completed report object (with baseline diff applied)
|
|
237
|
+
* @param {object} diff - Baseline diff returned by processReport
|
|
238
|
+
* @param {string} reportPath - Path to the written JSON report (for HTML generation)
|
|
239
|
+
*/
|
|
240
|
+
export async function dispatchAll(report, diff, reportPath) {
|
|
241
|
+
return startSpan('argus.dispatch', { baseUrl: report?.baseUrl ?? '' }, async () => {
|
|
242
|
+
if (isSlackConfigured()) {
|
|
243
|
+
try {
|
|
244
|
+
await startSpan('argus.dispatch', { channel: 'slack' }, () => dispatchToSlack(report, diff));
|
|
245
|
+
} catch (err) {
|
|
246
|
+
logger.error(`[ARGUS] Slack dispatch failed: ${err.message}`);
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
logger.info('\n[ARGUS] No Slack credentials — generating HTML report...');
|
|
250
|
+
const htmlPath = await startSpan('argus.dispatch', { channel: 'html' }, () => generateHtmlReport(reportPath));
|
|
251
|
+
logger.info(`[ARGUS] ✓ Open in browser: ${htmlPath}\n`);
|
|
252
|
+
openInBrowser(htmlPath);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (isGitHubConfigured()) {
|
|
256
|
+
try {
|
|
257
|
+
await startSpan('argus.dispatch', { channel: 'github' }, () => reportToGitHub(report, diff));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
logger.error(`[ARGUS] GitHub reporting failed: ${err.message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}); // end argus.dispatch span
|
|
263
|
+
}
|