diff --git a/packages/cli/src/utils.test.ts b/packages/cli/src/utils.test.ts new file mode 100644 index 0000000..f254ba3 --- /dev/null +++ b/packages/cli/src/utils.test.ts @@ -0,0 +1,91 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect } from 'bun:test'; +import { formatOutput } from './utils.js'; + +describe('formatOutput', () => { + describe('--format markdown', () => { + it('renders a lint report instead of [object Object]', () => { + const lintOutput = { + findings: [ + { severity: 'warning', path: 'colors.surface', message: "'surface' is defined but never referenced." }, + { severity: 'info', message: 'Design system defines 10 colors.' }, + ], + summary: { errors: 0, warnings: 1, infos: 1 }, + }; + + const result = formatOutput(lintOutput, { format: 'markdown' }); + expect(result).not.toContain('[object Object]'); + expect(result).toContain('# Lint Report'); + expect(result).toContain('**0 errors**'); + expect(result).toContain('**1 warnings**'); + expect(result).toContain('**1 infos**'); + expect(result).toContain("'surface' is defined but never referenced."); + expect(result).toContain('`colors.surface`'); + }); + + it('renders findings without a path', () => { + const lintOutput = { + findings: [ + { severity: 'info', message: 'Token count summary.' }, + ], + summary: { errors: 0, warnings: 0, infos: 1 }, + }; + + const result = formatOutput(lintOutput, { format: 'markdown' }); + expect(result).toContain('- **info**: Token count summary.'); + }); + + it('renders an empty findings section when there are none', () => { + const lintOutput = { + findings: [], + summary: { errors: 0, warnings: 0, infos: 0 }, + }; + + const result = formatOutput(lintOutput, { format: 'markdown' }); + expect(result).toContain('# Lint Report'); + expect(result).not.toContain('## Findings'); + }); + + it('handles the --format md alias', () => { + const lintOutput = { + findings: [], + summary: { errors: 0, warnings: 0, infos: 0 }, + }; + + const result = formatOutput(lintOutput, { format: 'md' }); + expect(result).toContain('# Lint Report'); + }); + + it('preserves legacy fixer shape with string summary', () => { + const fixerOutput = { + summary: 'Fixed 3 issues', + details: 'Some details here', + }; + + const result = formatOutput(fixerOutput, { format: 'markdown' }); + expect(result).toContain('# Fixed 3 issues'); + expect(result).toContain('## Details'); + }); + }); + + describe('default format (JSON)', () => { + it('returns valid JSON', () => { + const data = { findings: [], summary: { errors: 0, warnings: 0, infos: 0 } }; + const result = formatOutput(data, {}); + expect(JSON.parse(result)).toEqual(data); + }); + }); +}); diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 620f079..22756e4 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -52,25 +52,72 @@ export function formatOutput(data: unknown, args: { format?: string }): string { } function formatAsMarkdown(data: unknown): string { - if (typeof data === 'object' && data !== null) { - const obj = data as Record; - let result = ''; - if (obj.summary) { - result += `# ${obj.summary}\n\n`; - } - if (obj.details) { - result += `## Details\n\n`; - result += formatAsText(obj.details); - result += '\n'; - } - if (obj.patches && Array.isArray(obj.patches) && obj.patches.length > 0) { - result += `## Patches\n\n`; - result += formatAsText(obj.patches); - result += '\n'; + if (typeof data !== 'object' || data === null) { + return String(data); + } + + const obj = data as Record; + + // Lint output: { findings: [...], summary: { errors, warnings, infos } } + if (isLintOutput(obj)) { + return formatLintAsMarkdown(obj); + } + + // Legacy fixer/diff shape: { summary: string, details?, patches? } + let result = ''; + if (typeof obj.summary === 'string') { + result += `# ${obj.summary}\n\n`; + } + if (obj.details) { + result += `## Details\n\n`; + result += formatAsText(obj.details); + result += '\n'; + } + if (obj.patches && Array.isArray(obj.patches) && obj.patches.length > 0) { + result += `## Patches\n\n`; + result += formatAsText(obj.patches); + result += '\n'; + } + return result || formatAsText(data); +} + +interface LintSummary { + errors: number; + warnings: number; + infos: number; +} + +interface LintFinding { + severity: string; + message: string; + path?: string; +} + +function isLintOutput(obj: Record): boolean { + if (!Array.isArray(obj.findings)) return false; + if (typeof obj.summary !== 'object' || obj.summary === null) return false; + const s = obj.summary as Record; + return typeof s.errors === 'number' + && typeof s.warnings === 'number' + && typeof s.infos === 'number'; +} + +function formatLintAsMarkdown(obj: Record): string { + const summary = obj.summary as LintSummary; + const findings = obj.findings as LintFinding[]; + + let result = '# Lint Report\n\n'; + result += `**${summary.errors} errors**, **${summary.warnings} warnings**, **${summary.infos} infos**\n`; + + if (findings.length > 0) { + result += '\n## Findings\n\n'; + for (const f of findings) { + const location = f.path ? ` \`${f.path}\`:` : ':'; + result += `- **${f.severity}**${location} ${f.message}\n`; } - return result || formatAsText(data); } - return String(data); + + return result; } function formatAsText(data: unknown, indent = 0): string {