From 1d907fcdbe89861a4e05e656c515b2cb15e194a1 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 11 Oct 2026 17:35:40 -0400 Subject: [PATCH] fix(scan report): handle large alert output --- src/command/scan/output-scan-report.mts | 39 +++++++++++++++---- .../scan/output-scan-report-text.test.mts | 28 ++++++++++++- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/command/scan/output-scan-report.mts b/src/command/scan/output-scan-report.mts index b42b096c9..7a8af3962 100644 --- a/src/command/scan/output-scan-report.mts +++ b/src/command/scan/output-scan-report.mts @@ -86,9 +86,24 @@ export function formatAlertTable(rows: ReportAlertRow[]): string[] { row.introducedBy, row.manifest, ]) - const widths = headers.map((header, i) => - Math.max(header.length, ...cells.map(cell => (cell[i] ?? '').length)), - ) + const widths = headers.map(header => header.length) + for ( + let rowIndex = 0, { length: rowCount } = cells; + rowIndex < rowCount; + rowIndex += 1 + ) { + const cell = cells[rowIndex]! + for ( + let cellIndex = 0, { length: cellLength } = cell; + cellIndex < cellLength; + cellIndex += 1 + ) { + widths[cellIndex] = Math.max( + widths[cellIndex] ?? 0, + cell[cellIndex]?.length ?? 0, + ) + } + } const toRow = (values: string[]) => ` ${values .map((value, i) => value.padEnd(widths[i] ?? 0)) @@ -116,6 +131,12 @@ export function formatLabelledPairs(pairs: Array<[string, string]>): string[] { ) } +export function isFullScanReport( + report: ScanReport | { healthy: boolean }, +): report is ScanReport { + return 'alerts' in report +} + export async function outputGeneratedScanReport( scanReport: Extract, { ok: true }>, config: Pick< @@ -159,10 +180,10 @@ export async function outputGeneratedScanReport( if (short) { logger.log(scanReport.data.healthy ? 'OK' : 'ERR') + } else if (isFullScanReport(scanReport.data)) { + logger.log(toPlainTextReport(scanReport.data, includeLicensePolicy)) } else { - logger.log( - toPlainTextReport(scanReport.data as ScanReport, includeLicensePolicy), - ) + logger.log(scanReport.data.healthy ? 'OK' : 'ERR') } } @@ -401,8 +422,10 @@ export function toPlainTextReport( lines.push( ` ${rows.length} ${pluralize('alert', { count: rows.length })} with a policy set to at least "${reportLevel}".`, '', - ...formatAlertTable(rows), - '', ) + for (const line of formatAlertTable(rows)) { + lines.push(line) + } + lines.push('') return lines.join('\n') } diff --git a/test/unit/command/scan/output-scan-report-text.test.mts b/test/unit/command/scan/output-scan-report-text.test.mts index b0b604b6d..e21447e4f 100644 --- a/test/unit/command/scan/output-scan-report-text.test.mts +++ b/test/unit/command/scan/output-scan-report-text.test.mts @@ -16,7 +16,10 @@ import { toPlainTextReport, } from '../../../../src/command/scan/output-scan-report.mts' -import type { ScanReport } from '../../../../src/command/scan/report-generation.mts' +import type { + ReportLeafNode, + ScanReport, +} from '../../../../src/command/scan/report-generation.mts' // Matches any ANSI escape sequence, the codes that show up as literal noise // like "[32m" in a log viewer that does not interpret them. @@ -204,6 +207,29 @@ describe('toPlainTextReport', () => { expect(overlong).toEqual([]) }) + it('renders a large alert report without argument-count failures', () => { + const alertRow: ReportLeafNode = { + manifest: ['package.json'], + policy: 'error', + type: 'envVars', + url: '', + } + const packages = new Map>() + const alertCount = 200_000 + for (let i = 0; i < alertCount; i += 1) { + packages.set( + `package-${String(i).padStart(6, '0')}`, + new Map([['1.0.0', alertRow]]), + ) + } + const alerts: ScanReport['alerts'] = new Map([['npm', packages]]) + const text = toPlainTextReport(buildReport({ alerts, healthy: false })) + + expect(text).toContain('200000 alerts with a policy') + expect(text).toContain('package-000000') + expect(text).toContain('package-199999') + }) + it('says so plainly when there are no alerts', () => { const text = toPlainTextReport( buildReport({ alerts: new Map(), healthy: true }),