argusqa-os 9.7.5 → 9.7.6

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-738%2F738-4ADE80)](test-harness/)
7
+ [![Harness](https://img.shields.io/badge/harness-846%2F846-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).**
@@ -217,9 +217,10 @@ npm run report:html # Generate reports/report.html from last JSON audit
217
217
  npm run report:pdf # Export HTML report to A4 PDF (requires: npm install puppeteer)
218
218
  npm run server # Start Slack slash-command server (port 3001)
219
219
  npm run init # Interactive setup wizard
220
- npm run test:unit # 61 unit tests — no Chrome required
221
- npm run test:harness # 144-block correctness harness — requires Chrome
220
+ npm run test:unit # 94 unit tests — no Chrome required
221
+ npm run test:harness # 149-block correctness harness — requires Chrome
222
222
  npm run test:harness:log # same, but tees full output to harness-results.txt
223
+ npm run test:coverage # merged unit + harness coverage gate (requires Chrome)
223
224
  ```
224
225
 
225
226
  **Watch mode** — live monitoring as you develop:
@@ -342,7 +343,7 @@ Argus is a **complementary layer**, not a replacement for unit or E2E tests:
342
343
 
343
344
  ## Known Limitations
344
345
 
345
- All 738 harness assertions pass (`738/738`) — there are currently no known MCP- or Chrome-layer restrictions. Soft assertions (Lighthouse, performance traces) still require non-headless Chrome and are skipped in headless CI.
346
+ All 846 harness assertions pass (`846/846`) — there are currently no known MCP- or Chrome-layer restrictions. Lighthouse now runs in headless (after the `lighthouse_audit` argument fix); the remaining soft assertions (perf traces, GC-dependent heap-growth) are promoted to counted hard assertions only in the weekly strict-soft lane (`harness-strict.yml`) via `ARGUS_HARNESS_STRICT_SOFT`.
346
347
 
347
348
  ---
348
349
 
@@ -361,8 +362,8 @@ src/
361
362
  chrome-launcher.js — npm run chrome / argus-chrome — launches Chrome with correct flags
362
363
  doctor.js — npm run doctor / argus-doctor — pre-flight checks
363
364
  pr-validate.js — headless CI entry point for GitHub Actions
364
- test-harness/ — 144-block correctness harness, 738 hard assertions, 60 fixture pages
365
- test/unit/ — 61 Vitest unit tests (no Chrome required)
365
+ test-harness/ — 149-block correctness harness, 846 hard assertions, 63 fixture pages
366
+ test/unit/ — 94 Vitest unit tests (no Chrome required)
366
367
  landing/ — Product landing page (React 19 + Vite + Tailwind)
367
368
  ```
368
369
 
@@ -373,7 +374,7 @@ Full source map → [CLAUDE.md](CLAUDE.md) · MCP/DSL reference → [SKILL.md](S
373
374
  ## Contributing
374
375
 
375
376
  1. Fork the repo and create a branch
376
- 2. `npm run test:unit` — verify without Chrome (61 tests)
377
+ 2. `npm run test:unit` — verify without Chrome (94 tests)
377
378
  3. `npm run test:harness` — full integration coverage (requires Chrome on port 9222)
378
379
  4. Open a PR — Argus audits itself via the CI workflow
379
380
 
package/glama.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://glama.ai/mcp/schemas/server.json",
3
3
  "name": "argus",
4
- "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 9 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types), argus_visual_diff (screenshot baseline comparison, updateBaseline flag), argus_pr_validate (PR diff → affected routes → targeted audit → blocked flag). Every finding is post-processed with intelligent baseline filtering (cross-run noise classifier) and root cause linking (recent git commits mapped to new findings). 144 test blocks, 738 hard assertions, 67 detection categories.",
4
+ "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 9 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types), argus_visual_diff (screenshot baseline comparison, updateBaseline flag), argus_pr_validate (PR diff → affected routes → targeted audit → blocked flag). Every finding is post-processed with intelligent baseline filtering (cross-run noise classifier) and root cause linking (recent git commits mapped to new findings). 149 test blocks, 846 hard assertions, 67 detection categories.",
5
5
  "maintainers": ["ironclawdevs27"],
6
6
  "tools": [
7
7
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argusqa-os",
3
- "version": "9.7.5",
3
+ "version": "9.7.6",
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": [
@@ -53,6 +53,10 @@
53
53
  "test:harness:log": "node test-harness/run-with-log.mjs",
54
54
  "test:unit": "vitest run test/unit",
55
55
  "test": "npm run test:unit && npm run test:harness",
56
+ "coverage:unit": "vitest run test/unit --coverage",
57
+ "coverage:harness": "c8 npm run test:harness",
58
+ "coverage:gate": "node scripts/coverage-gate.mjs --min-lines 60 --allow-uncovered src/mcp-server.js,src/orchestration/env-comparison.js,src/server/index.js",
59
+ "test:coverage": "npm run coverage:harness && npm run coverage:unit && npm run coverage:gate",
56
60
  "report:html": "node src/utils/html-reporter.js",
57
61
  "report:pdf": "node src/utils/pdf-exporter.js",
58
62
  "mcp-server": "node src/mcp-server.js"
@@ -72,6 +76,10 @@
72
76
  "zod": "^4.4.3"
73
77
  },
74
78
  "devDependencies": {
79
+ "@vitest/coverage-v8": "^4.1.8",
80
+ "c8": "^10.1.3",
81
+ "fast-check": "^4.8.0",
82
+ "istanbul-lib-coverage": "^3.2.2",
75
83
  "vitest": "^4.1.8"
76
84
  }
77
85
  }
@@ -111,7 +111,19 @@ export class CdpBrowserAdapter {
111
111
  // is rejected with an Unknown-argument error). Callers still pass the numeric
112
112
  // requestId parsed from list_network_requests.
113
113
  getNetworkRequest(reqId) { return this._mcp.get_network_request({ reqid: reqId }); }
114
- lighthouse(url, opts = {}) { return this._mcp.lighthouse_audit({ url, ...opts }); }
114
+ // lighthouse_audit audits the CURRENTLY-NAVIGATED page and accepts only
115
+ // mode/device/outputDirPath. Passing `url` (or `categories`) is REJECTED with an
116
+ // "Unknown argument" error that comes back as RESOLVED text — so every Argus Lighthouse
117
+ // run silently no-op'd (caught upstream as "Lighthouse skipped", scores perpetually N/A).
118
+ // Navigate to the target first, then audit; strip url/categories defensively so legacy
119
+ // callers cannot reintroduce the rejected args. mode 'navigation' (the tool default)
120
+ // reloads + audits. Performance is intentionally excluded by lighthouse_audit (covered by
121
+ // the web-vitals analyzer) — it returns accessibility/best-practices/seo/agentic-browsing.
122
+ async lighthouse(url, opts = {}) {
123
+ if (url) await this.navigate(url);
124
+ const { url: _ignoredUrl, categories: _ignoredCats, ...valid } = opts;
125
+ return this._mcp.lighthouse_audit(valid);
126
+ }
115
127
  startTrace() { return this._mcp.performance_start_trace({}); }
116
128
  stopTrace() { return this._mcp.performance_stop_trace({}); }
117
129
  analyzeInsight(opts) { return this._mcp.performance_analyze_insight(opts); }
@@ -11,6 +11,6 @@
11
11
  * continue to import from this file unchanged.
12
12
  */
13
13
 
14
- export { runCrawl, crawlRouteCheap, crawlRouteExpensive } from './orchestrator.js';
14
+ export { runCrawl, crawlRouteCheap, crawlRouteExpensive, checkHttpsRequired } from './orchestrator.js';
15
15
  export { processReport, deduplicateFindings, rebuildSummary } from './report-processor.js';
16
16
  export { dispatchAll } from './dispatcher.js';
@@ -370,6 +370,33 @@ function analyzeNetworkPerformance(perfEntries, pageUrl) {
370
370
  return bugs;
371
371
  }
372
372
 
373
+ /**
374
+ * HTTPS-enforcement rule (single source of truth, exported so it can be verified
375
+ * directly — the harness can only crawl localhost, which is excluded, so the
376
+ * positive-trigger path has no live fixture).
377
+ *
378
+ * Returns a `security_no_https` finding for an http:// page on a non-loopback host,
379
+ * or null otherwise (https, or any localhost/127.x/::1 address).
380
+ *
381
+ * @param {string} url
382
+ * @returns {{type:string,message:string,severity:string,url:string}|null}
383
+ */
384
+ export function checkHttpsRequired(url) {
385
+ try {
386
+ const parsed = new URL(url);
387
+ const isLocalhost = /^(localhost|127\.|::1)/.test(parsed.hostname);
388
+ if (parsed.protocol === 'http:' && !isLocalhost) {
389
+ return {
390
+ type: 'security_no_https',
391
+ message: `Page served over HTTP — enforce HTTPS via server redirect or HSTS`,
392
+ severity: 'warning',
393
+ url,
394
+ };
395
+ }
396
+ } catch { /* URL parse failure */ }
397
+ return null;
398
+ }
399
+
373
400
  // ── Cheap Crawl (called ×2 for flakiness detection) ───────────────────────────
374
401
 
375
402
  /**
@@ -680,19 +707,9 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
680
707
  logger.warn(`[ARGUS] Issues analysis skipped for ${url}: ${err.message}`);
681
708
  }
682
709
 
683
- // 9f. HTTPS enforcement check
684
- try {
685
- const parsed = new URL(url);
686
- const isLocalhost = /^(localhost|127\.|::1)/.test(parsed.hostname);
687
- if (parsed.protocol === 'http:' && !isLocalhost) {
688
- result.errors.push({
689
- type: 'security_no_https',
690
- message: `Page served over HTTP — enforce HTTPS via server redirect or HSTS`,
691
- severity: 'warning',
692
- url,
693
- });
694
- }
695
- } catch { /* URL parse failure */ }
710
+ // 9f. HTTPS enforcement check (shared rule — see checkHttpsRequired above)
711
+ const httpsFinding = checkHttpsRequired(url);
712
+ if (httpsFinding) result.errors.push(httpsFinding);
696
713
 
697
714
  // 10. Deduplicate within this cheap run
698
715
  result.errors = deduplicateErrors(result.errors);
@@ -31,7 +31,10 @@ const CLASSIFIERS = [
31
31
  {
32
32
  type: 'cors_violation',
33
33
  issueTypePattern: /cors/i,
34
- textPattern: /cors policy|cross.origin.*blocked|access.control.allow.origin/i,
34
+ // Live Chrome 149 surfaces CorsIssue in the panel as e.g.
35
+ // "Ensure CORS response header values are valid" — the older phrase-specific
36
+ // patterns missed it, so the \bcors\b word-match anchors any CORS issue title.
37
+ textPattern: /cors policy|cross.origin.*blocked|access.control.allow.origin|\bcors\b/i,
35
38
  severity: (isCritical) => isCritical ? 'critical' : 'warning',
36
39
  },
37
40
  {
@@ -49,7 +52,10 @@ const CLASSIFIERS = [
49
52
  {
50
53
  type: 'cookie_attribute_missing',
51
54
  issueTypePattern: /cookie/i,
52
- textPattern: /samesite|secure attribute|partitioned|cookie.*rejected|set-cookie.*blocked/i,
55
+ // Live Chrome 149 surfaces the SameSite=None-without-Secure cookie Issue as
56
+ // "Mark cross-site cookies as Secure to allow setting them in cross-site contexts"
57
+ // — neither "samesite" nor "secure attribute" appear, so match those phrasings too.
58
+ textPattern: /samesite|secure attribute|partitioned|cookie.*rejected|set-cookie.*blocked|cross-site cookies?|cookies? as secure/i,
53
59
  severity: () => 'warning',
54
60
  },
55
61
  {
@@ -5,12 +5,49 @@
5
5
  * checkLighthouse directly without pulling in the Slack-initialised orchestrator.
6
6
  */
7
7
 
8
+ import fs from 'node:fs';
8
9
  import { registerExpensive } from '../registry.js';
9
10
  import { thresholds } from '../config/targets.js';
10
11
  import { childLogger } from './logger.js';
11
12
 
12
13
  const logger = childLogger('lighthouse-checker');
13
14
 
15
+ /**
16
+ * Parse a chrome-devtools-mcp `lighthouse_audit` response into the Lighthouse
17
+ * result shape this module consumes: `{ categories, audits }` (category scores 0–1,
18
+ * `audits` keyed by id). The tool returns markdown with a "### Reports" section that
19
+ * points at a full `report.json`; we read that for complete category scores +
20
+ * per-audit detail (`auditRefs`, `title`, `description`). If the file is unavailable
21
+ * we fall back to the markdown "### Category Scores" block (scores only, no audits).
22
+ * Returns `{ categories: {}, audits: {} }` when nothing parses — never throws.
23
+ *
24
+ * @param {string} responseText - raw lighthouse_audit response (markdown text)
25
+ * @returns {{ categories: object, audits: object }}
26
+ */
27
+ export function parseLighthouseReport(responseText) {
28
+ const text = String(responseText ?? '');
29
+ // Prefer the authoritative report.json (categories + auditRefs + per-audit detail).
30
+ const pathMatch = text.match(/([A-Za-z]:\\[^\r\n]*?report\.json|\/[^\r\n]*?report\.json)/);
31
+ if (pathMatch) {
32
+ try {
33
+ const json = JSON.parse(fs.readFileSync(pathMatch[1].trim(), 'utf8'));
34
+ if (json && typeof json === 'object' && json.categories) {
35
+ return { categories: json.categories, audits: json.audits ?? {} };
36
+ }
37
+ } catch { /* fall through to the markdown scores */ }
38
+ }
39
+ // Fallback: synthesize categories from the "### Category Scores" markdown block,
40
+ // e.g. "- Accessibility: 96 (accessibility)". Scores normalised to 0–1 to match report.json.
41
+ const categories = {};
42
+ const block = text.match(/### Category Scores\s*\n([\s\S]*?)(?:\n###|\s*$)/);
43
+ if (block) {
44
+ for (const m of block[1].matchAll(/^\s*-\s+.+?:\s*([\d.]+)\s*\(([\w-]+)\)\s*$/gm)) {
45
+ categories[m[2]] = { id: m[2], score: Number(m[1]) / 100 };
46
+ }
47
+ }
48
+ return { categories, audits: {} };
49
+ }
50
+
14
51
  const LIGHTHOUSE_LABELS = {
15
52
  accessibility: 'Accessibility',
16
53
  performance: 'Performance',
@@ -39,13 +76,16 @@ export async function checkLighthouse(browser, url) {
39
76
  const LIGHTHOUSE_TIMEOUT_MS = parseInt(process.env.ARGUS_LIGHTHOUSE_TIMEOUT ?? '120000', 10);
40
77
 
41
78
  try {
42
- const auditPromise = browser.lighthouse(url, {
43
- categories: ['accessibility', 'performance', 'seo', 'best-practices'],
44
- });
79
+ // browser.lighthouse navigates to url + audits the current page. lighthouse_audit
80
+ // returns markdown referencing a full report.json — parseLighthouseReport reads that
81
+ // back into the { categories, audits } shape this function consumes. Performance is
82
+ // excluded by the tool (covered by web-vitals); thresholds.lighthouse.performance is
83
+ // simply skipped below when its category is absent.
84
+ const auditPromise = browser.lighthouse(url);
45
85
  const timeoutPromise = new Promise((_, reject) =>
46
86
  setTimeout(() => reject(new Error(`Lighthouse timed out after ${LIGHTHOUSE_TIMEOUT_MS / 1000}s`)), LIGHTHOUSE_TIMEOUT_MS)
47
87
  );
48
- const result = await Promise.race([auditPromise, timeoutPromise]);
88
+ const result = parseLighthouseReport(await Promise.race([auditPromise, timeoutPromise]));
49
89
 
50
90
  const categories = result?.categories ?? {};
51
91
  const audits = result?.audits ?? {};