From ad659bde20667680a333a4baf5778873256276e4 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Thu, 18 Jun 2026 15:46:17 -0700 Subject: [PATCH] =?UTF-8?q?TEST-UNIFY-5a:=20cloud-audit=20adapter=20?= =?UTF-8?q?=E2=80=94=20ingest=20GraphDone-Cloud=20live-audit=20into=20the?= =?UTF-8?q?=20unified=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-repo unification (Core side). New tests/lib/adapters/cloud-audit.mjs reads a Cloud live-audit findings.json (the AuditRun output: {findings:[{dim,name,status, detail}]}) and maps pass/warn/fail/info → the unified counts, skipping cleanly when absent. Wired a 'cloud-audit' sequence into the manifest (full + report profiles, non-blocking) that auto-resolves the newest findings.json from the sibling ../GraphDone-Cloud/live-audit-artifacts (or --cloud-findings ). So `npm run test:unified --profile full` now reflects the live Cloud audit alongside the local unit/e2e/report sequences — one report across both repos. Verified: node:test 13/13 (incl. 3 new cloud-audit cases); ingesting the real findings.json reports 73✓ 5⚠ 11⏭, matching the live audit. (Fixed a status-key bug where warn landed in a stray counts key instead of 'warned'.) Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/lib/adapters/cloud-audit.mjs | 45 +++++++++++++++++++++++++ tests/lib/adapters/cloud-audit.test.mjs | 37 ++++++++++++++++++++ tests/run-unified.mjs | 7 ++++ tests/sequences/unified.config.mjs | 7 ++-- 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 tests/lib/adapters/cloud-audit.mjs create mode 100644 tests/lib/adapters/cloud-audit.test.mjs diff --git a/tests/lib/adapters/cloud-audit.mjs b/tests/lib/adapters/cloud-audit.mjs new file mode 100644 index 00000000..eca06666 --- /dev/null +++ b/tests/lib/adapters/cloud-audit.mjs @@ -0,0 +1,45 @@ +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * Optional ingest of the GraphDone-Cloud live-audit into the unified harness, so + * one `test:unified --profile full` can also reflect the live Cloud audit when + * both repos are checked out side by side. The Cloud audit (audit-kit AuditRun) + * writes findings.json = { target, stamp, findings:[{dim,name,status,detail}], ... } + * with status in pass|warn|fail|info. Skips cleanly when no findings file exists. + */ +const STATUS_MAP = { pass: 'passed', fail: 'failed', warn: 'warned', info: 'skipped' }; + +/** Newest live-audit-artifacts//findings.json under a Cloud repo dir, or null. */ +export function findLatestCloudFindings(cloudRepoDir) { + const root = join(cloudRepoDir, 'live-audit-artifacts'); + if (!existsSync(root)) return null; + const stamps = readdirSync(root) + .map((name) => join(root, name)) + .filter((p) => { try { return statSync(p).isDirectory() && existsSync(join(p, 'findings.json')); } catch { return false; } }) + .sort(); + return stamps.length ? join(stamps[stamps.length - 1], 'findings.json') : null; +} + +export function cloudAuditSequence({ id = 'cloud-audit', title = 'Cloud live audit', findingsPath } = {}) { + if (!findingsPath || !existsSync(findingsPath)) { + return { id, title, kind: 'audit', status: 'skipped', durationMs: 0, + counts: { passed: 0, failed: 0, warned: 0, skipped: 1 }, cases: [], + notes: 'no Cloud findings.json found (run GraphDone-Cloud live-audit, or check out the sibling repo)' }; + } + let data; + try { data = JSON.parse(readFileSync(findingsPath, 'utf8')); } catch (e) { + return { id, title, kind: 'audit', status: 'failed', durationMs: 0, + counts: { passed: 0, failed: 0, warned: 0, skipped: 0 }, cases: [{ title: 'parse findings.json', status: 'failed', error: String(e) }] }; + } + const counts = { passed: 0, failed: 0, warned: 0, skipped: 0 }; + const cases = []; + for (const f of data.findings || []) { + const status = STATUS_MAP[f.status] || 'skipped'; + counts[status]++; + if (status === 'failed' || status === 'warn') cases.push({ title: `[${f.dim}] ${f.name}`, status, error: f.detail || undefined }); + } + const status = counts.failed > 0 ? 'failed' : counts.warned > 0 ? 'warn' : counts.passed > 0 ? 'passed' : 'skipped'; + return { id, title: `${title} (${data.target || ''})`, kind: 'audit', status, durationMs: 0, counts, cases, + notes: `ingested ${findingsPath}` }; +} diff --git a/tests/lib/adapters/cloud-audit.test.mjs b/tests/lib/adapters/cloud-audit.test.mjs new file mode 100644 index 00000000..b74ad308 --- /dev/null +++ b/tests/lib/adapters/cloud-audit.test.mjs @@ -0,0 +1,37 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { writeFileSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { cloudAuditSequence } from './cloud-audit.mjs'; + +test('maps pass/warn/fail/info to unified counts + status', () => { + const dir = mkdtempSync(join(tmpdir(), 'ca-')); + const p = join(dir, 'findings.json'); + writeFileSync(p, JSON.stringify({ target: 'https://x', findings: [ + { dim: 'A', name: 'ok', status: 'pass' }, + { dim: 'A', name: 'meh', status: 'warn', detail: 'subscription' }, + { dim: 'B', name: 'note', status: 'info' }, + ] })); + const s = cloudAuditSequence({ findingsPath: p }); + assert.equal(s.counts.passed, 1); + assert.equal(s.counts.warned, 1); + assert.equal(s.counts.skipped, 1); + assert.equal(s.counts.failed, 0); + assert.equal(s.status, 'warn'); // warn dominates pass + assert.ok(!('warn' in s.counts) || typeof s.counts.warn !== 'number'); // no stray key +}); + +test('a failure makes the sequence failed', () => { + const dir = mkdtempSync(join(tmpdir(), 'ca-')); + const p = join(dir, 'findings.json'); + writeFileSync(p, JSON.stringify({ findings: [{ dim: 'X', name: 'broke', status: 'fail', detail: 'boom' }] })); + const s = cloudAuditSequence({ findingsPath: p }); + assert.equal(s.status, 'failed'); + assert.equal(s.cases.length, 1); +}); + +test('missing file → skipped, never throws', () => { + const s = cloudAuditSequence({ findingsPath: '/no/such/findings.json' }); + assert.equal(s.status, 'skipped'); +}); diff --git a/tests/run-unified.mjs b/tests/run-unified.mjs index 06938653..b8fa353f 100644 --- a/tests/run-unified.mjs +++ b/tests/run-unified.mjs @@ -12,6 +12,7 @@ import { mkdirSync, copyFileSync, rmSync } from 'node:fs'; import { join, extname, resolve } from 'node:path'; import { runVitestSequence } from './lib/adapters/vitest.mjs'; import { runPlaywrightSequence } from './lib/adapters/playwright.mjs'; +import { cloudAuditSequence, findLatestCloudFindings } from './lib/adapters/cloud-audit.mjs'; import { buildReport } from './lib/reporting/aggregate.mjs'; import { writeJsonReport } from './lib/reporting/json.mjs'; import { renderHtml } from './lib/reporting/html.mjs'; @@ -36,6 +37,12 @@ async function runOne(id) { process.stdout.write(`\n▶ ${id} — ${def.title}\n`); if (def.adapter === 'vitest') return runVitestSequence({ id, title: def.title, cwd: def.cwd }); if (def.adapter === 'playwright') return runPlaywrightSequence({ id, title: def.title, args: def.args, target }); + if (def.adapter === 'cloud-audit') { + const cloudDir = def.cloudRepoDir || resolve(process.cwd(), '..', 'GraphDone-Cloud'); + const explicit = flag('--cloud-findings', null); + const findingsPath = typeof explicit === 'string' ? explicit : findLatestCloudFindings(cloudDir); + return cloudAuditSequence({ id, title: def.title, findingsPath }); + } return { id, title: def.title, status: 'skipped', counts: { passed: 0, failed: 0, warned: 0, skipped: 1 }, cases: [] }; } diff --git a/tests/sequences/unified.config.mjs b/tests/sequences/unified.config.mjs index 325802c5..779d5fe4 100644 --- a/tests/sequences/unified.config.mjs +++ b/tests/sequences/unified.config.mjs @@ -24,11 +24,14 @@ export const SEQUENCES = { 'matrix': { adapter: 'playwright', title: 'Feature × resolution matrix', args: ['--project=matrix'], blocking: false }, 'vlm': { adapter: 'playwright', title: 'Local-VLM visual eval (skips w/o endpoints)', args: ['--project=vlm'], blocking: false }, 'perf-scale': { adapter: 'playwright', title: 'Large-scale perf sweep', args: ['--project=perf-scale'], blocking: false }, + // Cross-repo: ingest the newest GraphDone-Cloud live-audit findings.json (sibling + // repo) into the unified report. Skips cleanly when absent. + 'cloud-audit': { adapter: 'cloud-audit', title: 'Cloud live audit (ingested)', blocking: false }, }; export const PROFILES = { smoke: ['unit-web', 'smoke', 'e2e-auth'], pr: ['unit-web', 'smoke', 'e2e-auth', 'e2e-graph', 'mobile', 'perf-budgets'], - full: ['unit-web', 'smoke', 'e2e-auth', 'e2e-graph', 'mobile', 'perf-budgets', 'diagnostics', 'showcase', 'matrix', 'vlm', 'perf-scale'], - report: ['diagnostics', 'showcase', 'matrix', 'vlm', 'perf-scale'], + full: ['unit-web', 'smoke', 'e2e-auth', 'e2e-graph', 'mobile', 'perf-budgets', 'diagnostics', 'showcase', 'matrix', 'vlm', 'perf-scale', 'cloud-audit'], + report: ['diagnostics', 'showcase', 'matrix', 'vlm', 'perf-scale', 'cloud-audit'], };