fix: remove CI script tests, restore E2E coverage on PRs

- Remove coverage-slack-notify.test.ts (not needed for CI scripts)
- Un-export internal functions in coverage-slack-notify.ts
- Add pull_request trigger back to ci-tests-e2e-coverage.yaml so
  E2E coverage appears in PR report comments (runs in parallel,
  does not block other checks)
- Restore E2E coverage section in pr-report.yaml and unified-report.ts
This commit is contained in:
bymyself
2026-04-08 22:45:00 -07:00
parent b5f6d890f8
commit f8e2a8d1e3
5 changed files with 65 additions and 289 deletions

View File

@@ -4,6 +4,9 @@ on:
push:
branches: [main, core/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
concurrency:
group: e2e-coverage-${{ github.ref }}

View File

@@ -2,7 +2,7 @@ name: 'PR: Unified Report'
on:
workflow_run:
workflows: ['CI: Size Data', 'CI: Performance Report']
workflows: ['CI: Size Data', 'CI: Performance Report', 'CI: E2E Coverage']
types:
- completed
@@ -104,6 +104,25 @@ jobs:
path: temp/size-prev
if_no_artifact_found: warn
- name: Find coverage workflow run
if: steps.pr-meta.outputs.skip != 'true'
id: find-coverage
uses: ./.github/actions/find-workflow-run
with:
workflow-id: ci-tests-e2e-coverage.yaml
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
not-found-status: skip
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download coverage data
if: steps.pr-meta.outputs.skip != 'true' && steps.find-coverage.outputs.status == 'ready'
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: e2e-coverage
run_id: ${{ steps.find-coverage.outputs.run-id }}
path: temp/coverage
if_no_artifact_found: warn
- name: Download perf metrics (current)
if: steps.pr-meta.outputs.skip != 'true' && steps.find-perf.outputs.status == 'ready'
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
@@ -142,6 +161,7 @@ jobs:
pnpm exec tsx scripts/unified-report.ts
--size-status=${{ steps.find-size.outputs.status }}
--perf-status=${{ steps.find-perf.outputs.status }}
--coverage-status=${{ steps.find-coverage.outputs.status }}
> pr-report.md
- name: Remove legacy separate comments

View File

@@ -1,273 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { CoverageData } from './coverage-slack-notify'
import {
buildMilestoneBlock,
crossedMilestone,
formatCoverageRow,
formatDelta,
formatPct,
parseArgs,
parseLcovContent,
progressBar
} from './coverage-slack-notify'
describe('parseLcovContent', () => {
it('parses valid LCOV content', () => {
const content = ['LF:100', 'LH:75', 'end_of_record'].join('\n')
const result = parseLcovContent(content)
expect(result).toEqual({
percentage: 75,
totalLines: 100,
coveredLines: 75
})
})
it('sums lines across multiple records', () => {
const content = [
'LF:100',
'LH:50',
'end_of_record',
'LF:200',
'LH:150',
'end_of_record'
].join('\n')
const result = parseLcovContent(content)
expect(result).toEqual({
percentage: (200 / 300) * 100,
totalLines: 300,
coveredLines: 200
})
})
it('returns null for empty content', () => {
expect(parseLcovContent('')).toBeNull()
})
it('returns null when total lines is zero', () => {
const content = 'LH:10\nend_of_record'
expect(parseLcovContent(content)).toBeNull()
})
it('treats malformed numeric values as zero', () => {
const content = ['LF:abc', 'LH:10', 'LF:50', 'end_of_record'].join('\n')
const result = parseLcovContent(content)
expect(result).toEqual({
percentage: 20,
totalLines: 50,
coveredLines: 10
})
})
it('ignores unrelated lines', () => {
const content = [
'TN:',
'SF:/some/file.ts',
'LF:200',
'LH:100',
'end_of_record'
].join('\n')
const result = parseLcovContent(content)
expect(result).toEqual({
percentage: 50,
totalLines: 200,
coveredLines: 100
})
})
})
describe('progressBar', () => {
it('renders empty bar at 0%', () => {
expect(progressBar(0)).toBe('░'.repeat(20))
})
it('renders half-filled bar at 50%', () => {
const bar = progressBar(50)
expect(bar).toBe('█'.repeat(10) + '░'.repeat(10))
})
it('renders full bar at 100%', () => {
expect(progressBar(100)).toBe('█'.repeat(20))
})
it('clamps negative values to 0%', () => {
expect(progressBar(-10)).toBe('░'.repeat(20))
})
it('clamps values above 100% to 100%', () => {
expect(progressBar(150)).toBe('█'.repeat(20))
})
it('has consistent length of 20 characters', () => {
for (const pct of [0, 25, 50, 75, 100]) {
expect(progressBar(pct)).toHaveLength(20)
}
})
})
describe('formatPct', () => {
it('formats integer percentage', () => {
expect(formatPct(50)).toBe('50.0%')
})
it('formats decimal percentage', () => {
expect(formatPct(12.34)).toBe('12.3%')
})
it('formats zero', () => {
expect(formatPct(0)).toBe('0.0%')
})
it('formats 100', () => {
expect(formatPct(100)).toBe('100.0%')
})
})
describe('formatDelta', () => {
it('formats positive delta with + sign', () => {
expect(formatDelta(1.2)).toBe('+1.2%')
})
it('formats negative delta with - sign', () => {
expect(formatDelta(-0.5)).toBe('-0.5%')
})
it('formats zero delta with + sign', () => {
expect(formatDelta(0)).toBe('+0.0%')
})
it('formats large positive delta', () => {
expect(formatDelta(15.67)).toBe('+15.7%')
})
})
describe('crossedMilestone', () => {
it('detects crossing a 5% boundary', () => {
expect(crossedMilestone(23, 27)).toBe(25)
})
it('returns null when no boundary is crossed', () => {
expect(crossedMilestone(21, 24)).toBeNull()
})
it('detects exact boundary crossing', () => {
expect(crossedMilestone(24.9, 25)).toBe(25)
})
it('returns highest milestone when crossing multiple boundaries', () => {
expect(crossedMilestone(24, 31)).toBe(30)
})
it('returns null when values are equal', () => {
expect(crossedMilestone(25, 25)).toBeNull()
})
it('returns null when coverage decreases', () => {
expect(crossedMilestone(30, 28)).toBeNull()
})
it('detects crossing from 0', () => {
expect(crossedMilestone(0, 7)).toBe(5)
})
})
describe('buildMilestoneBlock', () => {
it('builds celebration block below target', () => {
const block = buildMilestoneBlock('Unit test', 50)
expect(block.type).toBe('section')
expect(block.text.type).toBe('mrkdwn')
expect(block.text.text).toContain('MILESTONE: Unit test coverage hit 50%')
expect(block.text.text).toContain('30 percentage points to go')
})
it('builds goal-reached block at target', () => {
const block = buildMilestoneBlock('Unit test', 80)
expect(block.text.text).toContain('GOAL REACHED')
expect(block.text.text).toContain('80%')
expect(block.text.text).toContain('✅')
})
it('builds goal-reached block above target', () => {
const block = buildMilestoneBlock('E2E test', 85)
expect(block.text.text).toContain('GOAL REACHED')
expect(block.text.text).toContain('85%')
})
it('uses singular "point" when 1 remaining', () => {
const block = buildMilestoneBlock('Unit test', 79)
expect(block.text.text).toContain('1 percentage point to go')
})
})
describe('parseArgs', () => {
it('parses all args', () => {
const argv = [
'--pr-url=https://github.com/org/repo/pull/42',
'--pr-number=42',
'--author=testuser'
]
expect(parseArgs(argv)).toEqual({
prUrl: 'https://github.com/org/repo/pull/42',
prNumber: '42',
author: 'testuser'
})
})
it('returns empty strings for missing args', () => {
expect(parseArgs([])).toEqual({
prUrl: '',
prNumber: '',
author: ''
})
})
it('handles args with empty values', () => {
const argv = ['--pr-url=', '--pr-number=', '--author=']
expect(parseArgs(argv)).toEqual({
prUrl: '',
prNumber: '',
author: ''
})
})
it('ignores unknown args', () => {
const argv = ['--unknown=value', '--pr-number=99']
expect(parseArgs(argv)).toEqual({
prUrl: '',
prNumber: '99',
author: ''
})
})
})
describe('formatCoverageRow', () => {
it('formats a coverage comparison row', () => {
const current: CoverageData = {
percentage: 55,
totalLines: 1000,
coveredLines: 550
}
const baseline: CoverageData = {
percentage: 50,
totalLines: 1000,
coveredLines: 500
}
const row = formatCoverageRow('Unit', current, baseline)
expect(row).toBe('*Unit:* 50.0% → 55.0% (+5.0%)')
})
it('formats a row with negative delta', () => {
const current: CoverageData = {
percentage: 48,
totalLines: 1000,
coveredLines: 480
}
const baseline: CoverageData = {
percentage: 50,
totalLines: 1000,
coveredLines: 500
}
const row = formatCoverageRow('E2E', current, baseline)
expect(row).toBe('*E2E:* 50.0% → 48.0% (-2.0%)')
})
})

View File

@@ -4,7 +4,7 @@ const TARGET = 80
const MILESTONE_STEP = 5
const BAR_WIDTH = 20
export interface CoverageData {
interface CoverageData {
percentage: number
totalLines: number
coveredLines: number
@@ -18,7 +18,7 @@ interface SlackBlock {
}
}
export function parseLcovContent(content: string): CoverageData | null {
function parseLcovContent(content: string): CoverageData | null {
let totalLines = 0
let coveredLines = 0
@@ -44,23 +44,23 @@ function parseLcov(filePath: string): CoverageData | null {
return parseLcovContent(readFileSync(filePath, 'utf-8'))
}
export function progressBar(percentage: number): string {
function progressBar(percentage: number): string {
const clamped = Math.max(0, Math.min(100, percentage))
const filled = Math.round((clamped / 100) * BAR_WIDTH)
const empty = BAR_WIDTH - filled
return '█'.repeat(filled) + '░'.repeat(empty)
}
export function formatPct(value: number): string {
function formatPct(value: number): string {
return value.toFixed(1) + '%'
}
export function formatDelta(delta: number): string {
function formatDelta(delta: number): string {
const sign = delta >= 0 ? '+' : ''
return sign + delta.toFixed(1) + '%'
}
export function crossedMilestone(prev: number, curr: number): number | null {
function crossedMilestone(prev: number, curr: number): number | null {
const prevBucket = Math.floor(prev / MILESTONE_STEP)
const currBucket = Math.floor(curr / MILESTONE_STEP)
@@ -70,10 +70,7 @@ export function crossedMilestone(prev: number, curr: number): number | null {
return null
}
export function buildMilestoneBlock(
label: string,
milestone: number
): SlackBlock {
function buildMilestoneBlock(label: string, milestone: number): SlackBlock {
if (milestone >= TARGET) {
return {
type: 'section',
@@ -102,7 +99,7 @@ export function buildMilestoneBlock(
}
}
export function parseArgs(argv: string[]): {
function parseArgs(argv: string[]): {
prUrl: string
prNumber: string
author: string
@@ -121,7 +118,7 @@ export function parseArgs(argv: string[]): {
return { prUrl, prNumber, author }
}
export function formatCoverageRow(
function formatCoverageRow(
label: string,
current: CoverageData,
baseline: CoverageData
@@ -213,6 +210,4 @@ function main() {
process.stdout.write(JSON.stringify(payload))
}
if (process.argv[1] === import.meta.filename) {
main()
}
main()

View File

@@ -11,6 +11,8 @@ function getArg(name: string): string | undefined {
const sizeStatus = getArg('size-status') ?? 'pending'
const perfStatus = getArg('perf-status') ?? 'pending'
const coverageStatus = getArg('coverage-status') ?? 'skip'
const lines: string[] = []
if (sizeStatus === 'ready') {
@@ -66,4 +68,33 @@ if (perfStatus === 'ready' && existsSync('test-results/perf-metrics.json')) {
lines.push('> ⏳ Performance tests in progress…')
}
if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
try {
const coverageReport = execFileSync(
'pnpm',
[
'exec',
'tsx',
'scripts/coverage-report.ts',
'temp/coverage/coverage.lcov'
],
{ encoding: 'utf-8' }
).trimEnd()
lines.push('')
lines.push(coverageReport)
} catch {
lines.push('')
lines.push('## 🔬 E2E Coverage')
lines.push('')
lines.push(
'> ⚠️ Failed to render coverage report. Check the CI workflow logs.'
)
}
} else if (coverageStatus === 'failed') {
lines.push('')
lines.push('## 🔬 E2E Coverage')
lines.push('')
lines.push('> ⚠️ Coverage collection failed. Check the CI workflow logs.')
}
process.stdout.write(lines.join('\n') + '\n')