argusqa-os 9.6.0 → 9.6.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/argusqa-os?color=7C3AED)](https://www.npmjs.com/package/argusqa-os)
6
6
  [![MCP Server](https://glama.ai/mcp/servers/ironclawdevs27/Argus/badges/card.svg)](https://glama.ai/mcp/servers/ironclawdevs27/Argus)
7
- [![Harness](https://img.shields.io/badge/harness-631%2F634-4ADE80)](test-harness/)
7
+ [![Harness](https://img.shields.io/badge/harness-641%2F644-4ADE80)](test-harness/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
9
 
10
10
  **Argus catches the bugs your test suite misses — visual regressions, API loops, CSS drift, console noise, accessibility failures, and more — and delivers rich reports to Slack (or a local HTML dashboard).**
@@ -62,7 +62,7 @@ Argus scans your app and either posts findings to Slack or opens a local `report
62
62
 
63
63
  ## What Argus Catches
64
64
 
65
- 31 analysis engines, 137 distinct issue types, zero test-file maintenance:
65
+ 31 analysis engines, 138 distinct issue types, zero test-file maintenance:
66
66
 
67
67
  | Category | What it detects |
68
68
  |---|---|
@@ -208,7 +208,7 @@ npm run report:html # Generate reports/report.html from last JSON audit
208
208
  npm run server # Start Slack slash-command server (port 3001)
209
209
  npm run init # Interactive setup wizard
210
210
  npm run test:unit # 61 unit tests — no Chrome required
211
- npm run test:harness # 137-block correctness harness — requires Chrome
211
+ npm run test:harness # 138-block correctness harness — requires Chrome
212
212
  ```
213
213
 
214
214
  **Watch mode** — live monitoring as you develop:
@@ -331,7 +331,7 @@ Argus is a **complementary layer**, not a replacement for unit or E2E tests:
331
331
 
332
332
  ## Known Limitations
333
333
 
334
- 3 permanent test failures (`631/634`) are MCP-layer restrictions — not fixable in Argus code:
334
+ 3 permanent test failures (`641/644`) are MCP-layer restrictions — not fixable in Argus code:
335
335
 
336
336
  | Tool | Constraint |
337
337
  |---|---|
@@ -351,7 +351,7 @@ src/
351
351
  adapters/browser.js — CdpBrowserAdapter — wraps all chrome-devtools-mcp calls
352
352
  config/targets.js — routes, thresholds, auth steps
353
353
  cli/init.js — argus init interactive setup wizard
354
- test-harness/ — 137-block correctness harness, 62 fixture pages
354
+ test-harness/ — 138-block correctness harness, 62 fixture pages
355
355
  test/unit/ — 61 Vitest unit tests (no Chrome required)
356
356
  landing/ — Product landing page (React 19 + Vite + Tailwind)
357
357
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argusqa-os",
3
- "version": "9.6.0",
3
+ "version": "9.6.1",
4
4
  "mcpName": "io.github.ironclawdevs27/argus",
5
5
  "description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
6
6
  "keywords": [
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Argus PR Validator — headless CI entry point for GitHub Actions.
4
+ *
5
+ * Environment variables (set by action.yml):
6
+ * ARGUS_PR_URL Full GitHub PR URL, e.g. https://github.com/owner/repo/pull/42 (required)
7
+ * TARGET_DEV_URL Base URL of the running application, e.g. https://staging.example.com (required)
8
+ * ARGUS_BLOCK_ON critical | warning | none (default: critical)
9
+ * GITHUB_TOKEN GitHub PAT or workflow GITHUB_TOKEN — optional for public repos
10
+ * ARGUS_ROUTES_FILE Path to a JSON routes array [{path,name}] — optional, see loadRoutes()
11
+ * GITHUB_OUTPUT Set by GitHub runner — path for step output key=value pairs
12
+ * GITHUB_STEP_SUMMARY Set by GitHub runner — path for markdown step summary
13
+ *
14
+ * Exit codes:
15
+ * 0 — audit passed (blocked=false) or no routes to audit
16
+ * 1 — audit blocked (findings at or above ARGUS_BLOCK_ON threshold) OR startup error
17
+ *
18
+ * Exports (used by test harness — no Chrome required):
19
+ * buildStepSummary(opts) → markdown string
20
+ * writeGithubOutputs(opts) → void (writes to GITHUB_OUTPUT file)
21
+ * writeStepSummary(markdown) → void (writes to GITHUB_STEP_SUMMARY file)
22
+ */
23
+
24
+ import fs from 'fs';
25
+ import path from 'path';
26
+ import { fileURLToPath } from 'url';
27
+ import { createMcpClient } from '../utils/mcp-client.js';
28
+ import { crawlRouteCheap } from '../orchestration/crawl-and-report.js';
29
+ import { parsePrUrl, fetchPrFiles, mapFilesToRoutes } from '../utils/pr-diff-analyzer.js';
30
+
31
+ // ── Exported helpers (testable without Chrome) ────────────────────────────────
32
+
33
+ /**
34
+ * Build a GitHub-flavoured markdown step summary.
35
+ *
36
+ * @param {object} opts
37
+ * @param {boolean} opts.blocked
38
+ * @param {{ critical: number, warning: number, info: number }} opts.summary
39
+ * @param {Array<{ path: string }>} opts.affectedRoutes
40
+ * @param {Array<{ route: string, critical: number, warning: number, info: number, error?: string }>} opts.perRoute
41
+ * @param {Array<object>} opts.findings
42
+ * @param {string[]} opts.changedFiles
43
+ * @param {string} opts.blockOn critical | warning | none
44
+ * @param {string} [opts.error] top-level error message (startup / fetch failure)
45
+ * @returns {string}
46
+ */
47
+ export function buildStepSummary({ blocked, summary, affectedRoutes, perRoute, findings, changedFiles, blockOn, error }) {
48
+ const icon = blocked ? '🔴' : summary.critical + summary.warning === 0 ? '✅' : '⚠️';
49
+ const status = blocked ? 'BLOCKED — merge prevented' : 'PASSED';
50
+
51
+ let md = `## ${icon} Argus PR Validator — ${status}\n\n`;
52
+
53
+ if (error) {
54
+ md += `> **Error:** ${String(error).replace(/`/g, "'")}\n\n`;
55
+ }
56
+
57
+ md += `| Metric | Value |\n|--------|-------|\n`;
58
+ md += `| Block threshold | \`${blockOn}\` |\n`;
59
+ md += `| Critical findings | **${summary.critical}** |\n`;
60
+ md += `| Warning findings | ${summary.warning} |\n`;
61
+ md += `| Info findings | ${summary.info} |\n`;
62
+ md += `| Routes audited | ${affectedRoutes.length} |\n`;
63
+ md += `| Files changed | ${changedFiles.length} |\n\n`;
64
+
65
+ if (perRoute.length > 0) {
66
+ md += `### Route Breakdown\n\n`;
67
+ md += `| Route | 🔴 Critical | ⚠️ Warning | ℹ️ Info |\n|-------|------------|-----------|--------|\n`;
68
+ for (const r of perRoute) {
69
+ const errNote = r.error ? ` _(error: ${String(r.error).slice(0, 60)})_` : '';
70
+ md += `| \`${r.route}\` | ${r.critical} | ${r.warning} | ${r.info}${errNote} |\n`;
71
+ }
72
+ md += '\n';
73
+ }
74
+
75
+ if (findings.length > 0) {
76
+ md += `### Findings\n\n`;
77
+ md += `| Severity | Type | Message | URL |\n|----------|------|---------|-----|\n`;
78
+ const shown = findings.slice(0, 50);
79
+ for (const f of shown) {
80
+ const sev = f.severity === 'critical' ? '🔴 critical'
81
+ : f.severity === 'warning' ? '⚠️ warning'
82
+ : 'ℹ️ info';
83
+ const msg = String(f.message ?? '').replace(/\|/g, '\\|').slice(0, 100);
84
+ const url = String(f.url ?? '').replace(/\|/g, '\\|').slice(0, 80);
85
+ md += `| ${sev} | \`${f.type ?? ''}\` | ${msg} | ${url} |\n`;
86
+ }
87
+ if (findings.length > 50) {
88
+ md += `\n_…and ${findings.length - 50} more findings._\n`;
89
+ }
90
+ md += '\n';
91
+ }
92
+
93
+ md += `---\n_Powered by [Argus QA](https://argus-qa.com)_\n`;
94
+ return md;
95
+ }
96
+
97
+ /**
98
+ * Write step outputs to $GITHUB_OUTPUT (key=value pairs).
99
+ * No-ops when GITHUB_OUTPUT is not set (local / non-Actions environments).
100
+ */
101
+ export function writeGithubOutputs({ blocked, summary, affectedRoutes }) {
102
+ const outputPath = process.env.GITHUB_OUTPUT;
103
+ if (!outputPath) return;
104
+ const routes = Array.isArray(affectedRoutes)
105
+ ? affectedRoutes.map(r => (typeof r === 'string' ? r : r.path)).join(',')
106
+ : '';
107
+ const lines = [
108
+ `blocked=${blocked}`,
109
+ `critical_count=${summary.critical}`,
110
+ `warning_count=${summary.warning}`,
111
+ `affected_routes=${routes}`,
112
+ ].join('\n') + '\n';
113
+ fs.appendFileSync(outputPath, lines);
114
+ }
115
+
116
+ /**
117
+ * Append markdown to $GITHUB_STEP_SUMMARY.
118
+ * No-ops when GITHUB_STEP_SUMMARY is not set.
119
+ */
120
+ export function writeStepSummary(markdown) {
121
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
122
+ if (!summaryPath) return;
123
+ fs.appendFileSync(summaryPath, markdown);
124
+ }
125
+
126
+ // ── Route loader ──────────────────────────────────────────────────────────────
127
+
128
+ async function loadRoutes() {
129
+ // 1. ARGUS_ROUTES_FILE env var
130
+ const routesFile = process.env.ARGUS_ROUTES_FILE;
131
+ if (routesFile) {
132
+ try {
133
+ const raw = JSON.parse(fs.readFileSync(routesFile, 'utf8'));
134
+ if (Array.isArray(raw) && raw.length > 0) {
135
+ console.log(`[argus] Loaded ${raw.length} route(s) from ${routesFile}`);
136
+ return raw;
137
+ }
138
+ } catch (err) {
139
+ console.error(`::warning::Could not parse ARGUS_ROUTES_FILE (${routesFile}): ${err.message}`);
140
+ }
141
+ }
142
+
143
+ // 2. argus.routes.json in working directory
144
+ const localFile = path.join(process.cwd(), 'argus.routes.json');
145
+ if (fs.existsSync(localFile)) {
146
+ try {
147
+ const raw = JSON.parse(fs.readFileSync(localFile, 'utf8'));
148
+ if (Array.isArray(raw) && raw.length > 0) {
149
+ console.log(`[argus] Loaded ${raw.length} route(s) from argus.routes.json`);
150
+ return raw;
151
+ }
152
+ } catch (err) {
153
+ console.error(`::warning::Could not parse argus.routes.json: ${err.message}`);
154
+ }
155
+ }
156
+
157
+ // 3. Default routes from the package's targets.js
158
+ try {
159
+ const { routes } = await import('../config/targets.js');
160
+ if (Array.isArray(routes) && routes.length > 0) {
161
+ return routes;
162
+ }
163
+ } catch { /* targets.js may not export routes in all environments */ }
164
+
165
+ // 4. Final fallback — audit root path only
166
+ console.log('[argus] No routes configured — falling back to root path audit');
167
+ return [{ path: '/', name: 'home' }];
168
+ }
169
+
170
+ // ── Main ──────────────────────────────────────────────────────────────────────
171
+
172
+ // Guard: run main() only when this script is executed directly, not when imported.
173
+ const _thisFile = fileURLToPath(import.meta.url);
174
+ if (process.argv[1] === _thisFile) {
175
+ await main();
176
+ }
177
+
178
+ async function main() {
179
+ const prUrl = process.env.ARGUS_PR_URL;
180
+ const targetUrl = process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
181
+ const blockOn = (process.env.ARGUS_BLOCK_ON ?? 'critical').toLowerCase().trim();
182
+ const token = process.env.GITHUB_TOKEN;
183
+
184
+ // Input validation
185
+ if (!prUrl) {
186
+ console.error('::error::ARGUS_PR_URL is not set. Set it to the full GitHub PR URL (e.g. https://github.com/owner/repo/pull/42).');
187
+ process.exit(1);
188
+ }
189
+ if (!['none', 'warning', 'critical'].includes(blockOn)) {
190
+ console.error(`::error::ARGUS_BLOCK_ON must be none | warning | critical, got: "${blockOn}"`);
191
+ process.exit(1);
192
+ }
193
+
194
+ let mcp;
195
+ const changedFiles = [];
196
+ const affectedRoutes = [];
197
+ const allFindings = [];
198
+ const perRoute = [];
199
+
200
+ try {
201
+ // Step 1: Fetch the PR file list from GitHub
202
+ console.log(`[argus] Fetching PR diff: ${prUrl}`);
203
+ const files = await fetchPrFiles(prUrl, token);
204
+ changedFiles.push(...files);
205
+ console.log(`[argus] ${files.length} changed file(s)`);
206
+
207
+ // Step 2: Map changed files to affected routes
208
+ const routes = await loadRoutes();
209
+ const affected = mapFilesToRoutes(files, routes);
210
+ affectedRoutes.push(...affected);
211
+
212
+ if (affected.length === 0) {
213
+ console.log('[argus] No affected routes resolved — skipping audit');
214
+ const summary = { critical: 0, warning: 0, info: 0 };
215
+ writeGithubOutputs({ blocked: false, summary, affectedRoutes: [] });
216
+ writeStepSummary(buildStepSummary({ blocked: false, summary, affectedRoutes: [], perRoute: [], findings: [], changedFiles: files, blockOn }));
217
+ process.exit(0);
218
+ }
219
+
220
+ console.log(`[argus] Auditing ${affected.length} route(s): ${affected.map(r => r.path).join(', ')}`);
221
+
222
+ // Step 3: Connect to Chrome via the chrome-devtools MCP client
223
+ console.log('[argus] Connecting to Chrome on port 9222...');
224
+ mcp = await createMcpClient();
225
+ console.log('[argus] Chrome connected.');
226
+
227
+ const baseOrigin = new URL(targetUrl).origin;
228
+
229
+ // Step 4: Audit each affected route via crawlRouteCheap
230
+ for (const route of affected) {
231
+ const url = new URL(route.path, targetUrl).href;
232
+ console.log(`[argus] → Auditing ${url}`);
233
+
234
+ try {
235
+ const raw = await crawlRouteCheap(route, baseOrigin, mcp);
236
+ const findings = Array.isArray(raw.errors) ? raw.errors : [];
237
+ allFindings.push(...findings);
238
+
239
+ const critical = findings.filter(f => f.severity === 'critical').length;
240
+ const warning = findings.filter(f => f.severity === 'warning').length;
241
+ const info = findings.filter(f => f.severity === 'info').length;
242
+ perRoute.push({ route: route.path, critical, warning, info });
243
+
244
+ console.log(`[argus] ${url}: ${critical} critical, ${warning} warning, ${info} info`);
245
+
246
+ // Emit inline GitHub Actions annotations for visible CI feedback
247
+ for (const f of findings.filter(g => g.severity === 'critical')) {
248
+ console.log(`::error::${String(f.message ?? '').replace(/\n/g, ' ')} [${f.type}] on ${url}`);
249
+ }
250
+ for (const f of findings.filter(g => g.severity === 'warning')) {
251
+ console.log(`::warning::${String(f.message ?? '').replace(/\n/g, ' ')} [${f.type}] on ${url}`);
252
+ }
253
+
254
+ } catch (routeErr) {
255
+ console.error(`::warning::Audit failed for ${url}: ${routeErr.message}`);
256
+ perRoute.push({ route: route.path, critical: 0, warning: 0, info: 0, error: routeErr.message });
257
+ }
258
+ }
259
+
260
+ // Step 5: Compute aggregate summary and merge-block decision
261
+ const summary = {
262
+ critical: allFindings.filter(f => f.severity === 'critical').length,
263
+ warning: allFindings.filter(f => f.severity === 'warning').length,
264
+ info: allFindings.filter(f => f.severity === 'info').length,
265
+ };
266
+
267
+ const blocked =
268
+ blockOn === 'critical' ? summary.critical > 0 :
269
+ blockOn === 'warning' ? summary.critical + summary.warning > 0 :
270
+ false;
271
+
272
+ // Step 6: Write GitHub Actions outputs and step summary
273
+ writeGithubOutputs({ blocked, summary, affectedRoutes: affected });
274
+ writeStepSummary(buildStepSummary({ blocked, summary, affectedRoutes: affected, perRoute, findings: allFindings, changedFiles: files, blockOn }));
275
+
276
+ // Step 7: Emit JSON result to stdout for downstream pipeline steps
277
+ const result = {
278
+ prUrl, targetUrl,
279
+ affectedRoutes: affected.map(r => r.path),
280
+ changedFiles: files,
281
+ findings: allFindings,
282
+ perRoute,
283
+ summary,
284
+ blocked,
285
+ blockOn,
286
+ };
287
+ console.log(JSON.stringify(result, null, 2));
288
+
289
+ if (blocked) {
290
+ console.error(`::error::Argus PR Validator: ${summary.critical} critical finding(s) found. Merge blocked (block-on=${blockOn}).`);
291
+ process.exit(1);
292
+ }
293
+
294
+ console.log(`[argus] ✓ Audit passed — ${summary.critical} critical, ${summary.warning} warning, ${summary.info} info.`);
295
+ process.exit(0);
296
+
297
+ } catch (err) {
298
+ const summary = { critical: 0, warning: 0, info: 0 };
299
+ console.error(`::error::Argus PR validation failed: ${err.message}`);
300
+ writeGithubOutputs({ blocked: false, summary, affectedRoutes: [] });
301
+ writeStepSummary(buildStepSummary({ blocked: false, summary, affectedRoutes: [], perRoute: [], findings: [], changedFiles, blockOn, error: err.message }));
302
+ process.exit(1);
303
+
304
+ } finally {
305
+ if (mcp) {
306
+ try { mcp.close(); } catch { /* ignore teardown errors */ }
307
+ }
308
+ }
309
+ }
package/src/mcp-server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Argus MCP Server (v9.6.0)
3
+ * Argus MCP Server (v9.6.1)
4
4
  *
5
5
  * Exposes Argus as an MCP server so Claude (or any MCP client) can call
6
6
  * argus_audit, argus_audit_full, argus_compare, argus_last_report, and
@@ -447,7 +447,7 @@ async function handleLastReport() {
447
447
  // ── Server bootstrap ──────────────────────────────────────────────────────────
448
448
 
449
449
  const server = new Server(
450
- { name: 'argus', version: '9.6.0' },
450
+ { name: 'argus', version: '9.6.1' },
451
451
  { capabilities: { tools: {} } },
452
452
  );
453
453