From ee4ba8353530f30d7301c28e69b1b1019621ab8e Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 25 Aug 2026 14:10:05 -0300 Subject: [PATCH] =?UTF-8?q?fix(ops96-audit):=20parseVitestStdout=20tolera?= =?UTF-8?q?=20sufixo=20do=20pnpm=20em=20suite=20vermelha=20=E2=80=94=20ret?= =?UTF-8?q?rato=20impresso=20mesmo=20com=20falha=20(contrato=20do=20metric?= =?UTF-8?q?s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/lib/testing-audit-metrics-core.mjs | 34 ++++++++++++++++++ scripts/testing-audit-metrics.mjs | 20 ++++------- .../unit/testingAuditMetricsCore.unit.spec.ts | 36 +++++++++++++++++++ 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/scripts/lib/testing-audit-metrics-core.mjs b/scripts/lib/testing-audit-metrics-core.mjs index 9dec2da1..afa91aa2 100644 --- a/scripts/lib/testing-audit-metrics-core.mjs +++ b/scripts/lib/testing-audit-metrics-core.mjs @@ -9,6 +9,40 @@ export const DEFAULT_TOP = 15 export const ms = (value) => `${(value / 1000).toFixed(1)}s` +/** Vitest's jest-compatible JSON payload starts with this key. */ +const VITEST_JSON_ANCHOR = '{"numTotal' + +/** + * Extract the Vitest JSON report from the runner's raw stdout. pnpm echoes the + * script line BEFORE the JSON and appends its own noise (e.g. the `ELIFECYCLE` + * notice) AFTER it when the suite is red; tests may also print console blobs + * with braces around the payload. Walking back from the last `}` finds the + * payload boundary, so a red suite still yields its portrait. + * + * @param {string} stdout raw stdout of `vitest run --reporter=json` + * @returns {Record} parsed JSON report + */ +export const parseVitestStdout = (stdout) => { + if (typeof stdout !== 'string' || stdout.length === 0) { + throw new Error('vitest não produziu relatório JSON no stdout') + } + const jsonStart = stdout.indexOf(VITEST_JSON_ANCHOR) + if (jsonStart === -1) { + throw new Error('vitest não produziu relatório JSON no stdout') + } + let end = stdout.length + while (end > jsonStart) { + end = stdout.lastIndexOf('}', end - 1) + if (end < jsonStart) break + try { + return JSON.parse(stdout.slice(jsonStart, end + 1)) + } catch { + // not the payload boundary yet — keep walking back + } + } + throw new Error('relatório JSON inválido') +} + /** @returns {number} */ const fileDurationMs = (entry) => { if (typeof entry.endTime === 'number' && typeof entry.startTime === 'number') { diff --git a/scripts/testing-audit-metrics.mjs b/scripts/testing-audit-metrics.mjs index 5fec20ee..26b428ea 100644 --- a/scripts/testing-audit-metrics.mjs +++ b/scripts/testing-audit-metrics.mjs @@ -23,6 +23,7 @@ import { DEFAULT_TOP, inventoryE2ESource, ms, + parseVitestStdout, renderE2EInventoryTable, renderSlowestFilesTable, summarizeVitestReport, @@ -43,9 +44,6 @@ const topFlag = args.indexOf('--top') const top = topFlag === -1 ? DEFAULT_TOP : Number(args[topFlag + 1]) if (!Number.isInteger(top) || top <= 0) usage() -/** Vitest's jest-compatible JSON payload starts with this key. */ -const VITEST_JSON_ANCHOR = '{"numTotal' - const runVitestLayer = async (layer) => { console.log(`[metrics] rodando pnpm test:${layer} --reporter=json …`) const result = spawnSync('pnpm', [`test:${layer}`, '--reporter=json'], { @@ -53,19 +51,15 @@ const runVitestLayer = async (layer) => { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, }) - const jsonStart = (result.stdout ?? '').indexOf(VITEST_JSON_ANCHOR) - if (jsonStart === -1) { - console.error(`[metrics] vitest não produziu relatório JSON no stdout (exit ${result.status})`) - console.error((result.stderr ?? '').slice(-4000)) - process.exit(result.status === 0 ? 2 : result.status) - } let report try { - report = JSON.parse(result.stdout.slice(jsonStart)) + report = parseVitestStdout(result.stdout ?? '') } catch (error) { - console.error( - `[metrics] relatório JSON inválido: ${error instanceof Error ? error.message : error}`, - ) + console.error(`[metrics] ${error instanceof Error ? error.message : error}`) + if (result.status !== 0) { + console.error((result.stderr ?? '').slice(-4000)) + process.exit(result.status) + } process.exit(2) } const summary = summarizeVitestReport(report) diff --git a/tests/unit/testingAuditMetricsCore.unit.spec.ts b/tests/unit/testingAuditMetricsCore.unit.spec.ts index 439b2fdf..18638b87 100644 --- a/tests/unit/testingAuditMetricsCore.unit.spec.ts +++ b/tests/unit/testingAuditMetricsCore.unit.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { inventoryE2ESource, + parseVitestStdout, renderE2EInventoryTable, renderSlowestFilesTable, summarizeVitestReport, @@ -75,6 +76,41 @@ describe('summarizeVitestReport', () => { }) }) +describe('parseVitestStdout', () => { + const json = '{"numTotalTestSuites":1,"numTotalTests":3,"numFailedTests":1,"testResults":[]}' + + it('parses the payload when pnpm echoes the script before and appends noise after (red suite)', () => { + const stdout = `\n> teqo@1.0.0 test:int /repo\n> cross-env … vitest run\n\n${json}\n ELIFECYCLE Command failed with exit code 1.\n` + + expect(parseVitestStdout(stdout)).toEqual({ + numTotalTestSuites: 1, + numTotalTests: 3, + numFailedTests: 1, + testResults: [], + }) + }) + + it('parses the payload when test console noise with braces follows the report', () => { + const stdout = `${json}\nconsole output { "not": "json" } trailing\n` + + expect(parseVitestStdout(stdout).numTotalTests).toBe(3) + }) + + it('parses a bare payload with surrounding whitespace', () => { + expect(parseVitestStdout(` ${json}\n`).numFailedTests).toBe(1) + }) + + it('rejects output without the vitest anchor', () => { + expect(() => parseVitestStdout('no json here')).toThrow( + 'vitest não produziu relatório JSON no stdout', + ) + }) + + it('rejects an anchor without a valid JSON boundary', () => { + expect(() => parseVitestStdout('{"numTotal"not json')).toThrow('relatório JSON inválido') + }) +}) + describe('renderSlowestFilesTable', () => { it('renders a markdown table limited to top N rows', () => { const table = renderSlowestFilesTable(summarizeVitestReport(reportFixture()), { top: 1 })