mirror of
https://github.com/NanmiCoder/cc-haha.git
synced 2026-06-20 00:18:03 +08:00
Separate quarantine review enforcement from server and coverage file selection so expired review dates fail only the governance lane. Refresh stale server quarantine suites and keep only the live provider test quarantined for non-live PR gates. Tested: bun run check:policy Tested: bun run check:server Tested: bun run check:coverage Tested: bun run verify Tested: git diff --check Confidence: high Scope-risk: moderate
270 lines
7.3 KiB
TypeScript
270 lines
7.3 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import {
|
|
collectServerTestFiles,
|
|
evaluateChangedLineCoverage,
|
|
evaluateThresholds,
|
|
parseChangedLinesFromDiff,
|
|
parseLcov,
|
|
prefixRelativeLcovSourcePaths,
|
|
} from './coverage'
|
|
|
|
describe('coverage gate helpers', () => {
|
|
test('parses lcov totals into percentages', () => {
|
|
const summary = parseLcov([
|
|
'TN:',
|
|
'SF:src/a.ts',
|
|
'FNF:4',
|
|
'FNH:3',
|
|
'BRF:10',
|
|
'BRH:7',
|
|
'LF:20',
|
|
'LH:18',
|
|
'end_of_record',
|
|
'SF:src/b.ts',
|
|
'FNF:1',
|
|
'FNH:1',
|
|
'LF:10',
|
|
'LH:5',
|
|
'end_of_record',
|
|
].join('\n'))
|
|
|
|
expect(summary.lines.pct).toBe(76.67)
|
|
expect(summary.functions.pct).toBe(80)
|
|
expect(summary.branches.pct).toBe(70)
|
|
})
|
|
|
|
test('filters lcov records to an explicit source scope', () => {
|
|
const summary = parseLcov([
|
|
'TN:',
|
|
'SF:/repo/src/server/routes.ts',
|
|
'LF:10',
|
|
'LH:8',
|
|
'FNF:2',
|
|
'FNH:2',
|
|
'end_of_record',
|
|
'SF:/repo/desktop/src-tauri/target/generated.js',
|
|
'LF:100',
|
|
'LH:0',
|
|
'FNF:10',
|
|
'FNH:0',
|
|
'end_of_record',
|
|
].join('\n'), {
|
|
rootDir: '/repo',
|
|
scope: {
|
|
id: 'server-api',
|
|
title: 'Server/API',
|
|
includePrefixes: ['src/server/'],
|
|
},
|
|
})
|
|
|
|
expect(summary.lines.pct).toBe(80)
|
|
expect(summary.functions.pct).toBe(100)
|
|
})
|
|
|
|
test('prefixes package-relative lcov source paths for changed-line coverage', () => {
|
|
const lcov = prefixRelativeLcovSourcePaths([
|
|
'TN:',
|
|
'SF:src/stores/updateStore.ts',
|
|
'LF:1',
|
|
'LH:1',
|
|
'end_of_record',
|
|
'SF:/repo/desktop/src/main.tsx',
|
|
'LF:1',
|
|
'LH:0',
|
|
'end_of_record',
|
|
'SF:desktop/src/App.tsx',
|
|
'LF:1',
|
|
'LH:1',
|
|
'end_of_record',
|
|
].join('\n'), 'desktop')
|
|
|
|
expect(lcov).toContain('SF:desktop/src/stores/updateStore.ts')
|
|
expect(lcov).toContain('SF:/repo/desktop/src/main.tsx')
|
|
expect(lcov).toContain('SF:desktop/src/App.tsx')
|
|
})
|
|
|
|
test('evaluates changed executable line coverage', () => {
|
|
const changedLines = parseChangedLinesFromDiff([
|
|
'diff --git a/src/server/routes.ts b/src/server/routes.ts',
|
|
'--- a/src/server/routes.ts',
|
|
'+++ b/src/server/routes.ts',
|
|
'@@ -10,0 +11,2 @@',
|
|
'+const covered = true',
|
|
'+const uncovered = false',
|
|
].join('\n'))
|
|
|
|
const failures = evaluateChangedLineCoverage(
|
|
changedLines,
|
|
new Map([
|
|
['src/server/routes.ts', {
|
|
suiteId: 'server-api',
|
|
executableLines: new Set([11, 12]),
|
|
coveredLines: new Set([11]),
|
|
}],
|
|
]),
|
|
[{
|
|
id: 'server-api',
|
|
title: 'Server/API',
|
|
includePrefixes: ['src/server/'],
|
|
}],
|
|
90,
|
|
).failures
|
|
|
|
expect(failures).toEqual(['changed-lines: coverage 50% is below minimum 90%'])
|
|
})
|
|
|
|
test('excludes non-instrumented desktop styles from changed-line coverage', () => {
|
|
const changedLines = parseChangedLinesFromDiff([
|
|
'diff --git a/desktop/src/theme/globals.css b/desktop/src/theme/globals.css',
|
|
'--- a/desktop/src/theme/globals.css',
|
|
'+++ b/desktop/src/theme/globals.css',
|
|
'@@ -10,0 +11,2 @@',
|
|
'+.sidebar {',
|
|
'+ color: var(--color-text-primary);',
|
|
'+}',
|
|
'diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx',
|
|
'--- a/desktop/src/main.tsx',
|
|
'+++ b/desktop/src/main.tsx',
|
|
'@@ -20,0 +21,1 @@',
|
|
'+bootstrapDesktopApp()',
|
|
].join('\n'))
|
|
|
|
const result = evaluateChangedLineCoverage(
|
|
changedLines,
|
|
new Map([
|
|
['desktop/src/main.tsx', {
|
|
suiteId: 'desktop',
|
|
executableLines: new Set([21]),
|
|
coveredLines: new Set([21]),
|
|
}],
|
|
]),
|
|
[{
|
|
id: 'desktop',
|
|
title: 'Desktop',
|
|
includePrefixes: ['desktop/src/'],
|
|
excludeSuffixes: ['.css'],
|
|
}],
|
|
90,
|
|
)
|
|
|
|
expect(result.files).toEqual([{
|
|
file: 'desktop/src/main.tsx',
|
|
suiteId: 'desktop',
|
|
covered: 1,
|
|
total: 1,
|
|
pct: 100,
|
|
}])
|
|
expect(result.failures).toEqual([])
|
|
})
|
|
|
|
test('reports minimum threshold failures', () => {
|
|
const failures = evaluateThresholds([
|
|
{
|
|
id: 'root-server',
|
|
title: 'Root',
|
|
status: 'passed',
|
|
command: ['bun', 'test'],
|
|
durationMs: 1,
|
|
logPath: 'coverage.log',
|
|
summary: {
|
|
lines: { total: 100, covered: 79, pct: 79 },
|
|
functions: { total: 10, covered: 9, pct: 90 },
|
|
branches: { total: 10, covered: 8, pct: 80 },
|
|
statements: { total: 100, covered: 79, pct: 79 },
|
|
},
|
|
},
|
|
], {
|
|
schemaVersion: 1,
|
|
minimums: {
|
|
'root-server': {
|
|
lines: 80,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(failures).toEqual(['root-server: lines coverage 79% is below minimum 80%'])
|
|
})
|
|
|
|
test('treats failed suite execution as a coverage failure', () => {
|
|
const failures = evaluateThresholds([
|
|
{
|
|
id: 'desktop',
|
|
title: 'Desktop',
|
|
status: 'failed',
|
|
command: ['bun', 'run', 'test'],
|
|
durationMs: 1,
|
|
logPath: 'coverage.log',
|
|
error: 'coverage command exited with 1',
|
|
},
|
|
], {
|
|
schemaVersion: 1,
|
|
minimums: {},
|
|
})
|
|
|
|
expect(failures).toEqual(['desktop: coverage command exited with 1'])
|
|
})
|
|
|
|
test('collects non-quarantined server tests when review windows have expired', () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'cc-haha-coverage-'))
|
|
try {
|
|
mkdirSync(join(root, 'src/server/__tests__'), { recursive: true })
|
|
mkdirSync(join(root, 'src/tools'), { recursive: true })
|
|
mkdirSync(join(root, 'src/utils'), { recursive: true })
|
|
writeFileSync(join(root, 'src/server/__tests__/active.test.ts'), '')
|
|
writeFileSync(join(root, 'src/server/__tests__/quarantined.test.ts'), '')
|
|
|
|
const files = collectServerTestFiles(root, {
|
|
quarantined: [
|
|
{
|
|
id: 'server:expired',
|
|
path: 'src/server/__tests__/quarantined.test.ts',
|
|
reason: 'Known instability under review.',
|
|
owner: 'maintainers',
|
|
reviewAfter: '2026-01-01',
|
|
exitCriteria: 'Make deterministic or remove from quarantine.',
|
|
},
|
|
],
|
|
})
|
|
|
|
expect(files).toEqual(['src/server/__tests__/active.test.ts'])
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('does not require every suite to exist in the ratchet baseline', () => {
|
|
const failures = evaluateThresholds([
|
|
{
|
|
id: 'new-suite',
|
|
title: 'New suite',
|
|
status: 'passed',
|
|
command: ['bun', 'test'],
|
|
durationMs: 1,
|
|
logPath: 'coverage.log',
|
|
summary: {
|
|
lines: { total: 100, covered: 90, pct: 90 },
|
|
functions: { total: 10, covered: 9, pct: 90 },
|
|
branches: { total: 10, covered: 8, pct: 80 },
|
|
statements: { total: 100, covered: 90, pct: 90 },
|
|
},
|
|
},
|
|
], {
|
|
schemaVersion: 1,
|
|
minimums: {
|
|
'new-suite': {
|
|
branches: 85,
|
|
},
|
|
},
|
|
ratchet: {
|
|
baselinePath: 'scripts/quality-gate/coverage-baseline.json',
|
|
allowedDropPercent: 0,
|
|
},
|
|
})
|
|
|
|
expect(failures).toEqual(['new-suite: branches coverage 80% is below minimum 85%'])
|
|
})
|
|
})
|