fix(ops96-audit): parseVitestStdout tolera sufixo do pnpm em suite vermelha — retrato impresso mesmo com falha (contrato do metrics)

This commit is contained in:
Francisco Solla 2026-08-25 14:10:05 -03:00
parent 148a9dc542
commit ee4ba83535
3 changed files with 77 additions and 13 deletions

View file

@ -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<string, unknown>} 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') {

View file

@ -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)

View file

@ -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 })