Compare commits

...

4 Commits

Author SHA1 Message Date
huang47
27ba172198 ci: enforce exact critical unit coverage 2026-07-14 12:27:06 -07:00
huang47
5c76c00af1 ci: find exact workflow artifacts 2026-07-14 12:27:06 -07:00
huang47
e02c4a8e4a ci: compare coverage across Git revisions 2026-07-14 12:27:06 -07:00
huang47
e1d23d6126 ci: add critical coverage reports 2026-07-14 12:27:06 -07:00
11 changed files with 2055 additions and 68 deletions

View File

@@ -8,17 +8,25 @@ inputs:
head-sha:
description: The commit SHA to find runs for
required: true
event:
description: Optional workflow event to match
required: false
default: ''
not-found-status:
description: Status to output when no run exists
description: Status to output when no matching run exists after polling
required: false
default: pending
wait-seconds:
description: Maximum time to poll for a matching successful run
required: false
default: '0'
token:
description: GitHub token for API access
required: true
outputs:
status:
description: One of 'ready', 'pending', 'failed', or the not-found-status value
description: One of 'ready', 'pending', 'failed', or the configured not-found status
value: ${{ steps.find.outputs.status }}
run-id:
description: The workflow run ID (only set when status is 'ready')
@@ -33,33 +41,94 @@ runs:
env:
WORKFLOW_ID: ${{ inputs.workflow-id }}
HEAD_SHA: ${{ inputs.head-sha }}
EVENT_NAME: ${{ inputs.event }}
NOT_FOUND_STATUS: ${{ inputs.not-found-status }}
WAIT_SECONDS: ${{ inputs.wait-seconds }}
with:
github-token: ${{ inputs.token }}
script: |
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: process.env.WORKFLOW_ID,
head_sha: process.env.HEAD_SHA,
per_page: 1,
});
const run = runs.workflow_runs[0];
if (!run) {
core.setOutput('status', process.env.NOT_FOUND_STATUS);
const waitSeconds = Number(process.env.WAIT_SECONDS);
if (!Number.isSafeInteger(waitSeconds) || waitSeconds < 0) {
core.setFailed('wait-seconds must be a non-negative integer');
return;
}
if (run.status !== 'completed') {
core.setOutput('status', 'pending');
return;
}
const deadline = Date.now() + waitSeconds * 1000;
if (run.conclusion !== 'success') {
core.setOutput('status', 'failed');
return;
}
while (true) {
let run;
let response;
let requestFailed = false;
try {
response = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: process.env.WORKFLOW_ID,
head_sha: process.env.HEAD_SHA,
event: process.env.EVENT_NAME || undefined,
per_page: 10,
});
} catch (error) {
const status = error?.status;
const retryable =
typeof status !== 'number' ||
status === 408 ||
status === 429 ||
status >= 500;
core.setOutput('status', 'ready');
core.setOutput('run-id', String(run.id));
if (!retryable) {
core.setFailed(
`listWorkflowRuns failed with status ${status}: ${String(error)}`
);
return;
}
requestFailed = true;
core.warning(`listWorkflowRuns failed: ${String(error)}`);
}
if (response) {
const workflowRuns = response.data.workflow_runs;
const successfulRun = workflowRuns.find(
({ status, conclusion }) =>
status === 'completed' && conclusion === 'success'
);
if (successfulRun) {
core.setOutput('status', 'ready');
core.setOutput('run-id', String(successfulRun.id));
return;
}
run =
workflowRuns.find(({ status }) => status !== 'completed') ??
workflowRuns[0];
}
if (run?.status === 'completed') {
if (run.conclusion === 'success') {
core.setOutput('status', 'ready');
core.setOutput('run-id', String(run.id));
} else {
core.setOutput('status', 'failed');
}
return;
}
const remainingMilliseconds = deadline - Date.now();
if (remainingMilliseconds <= 0) {
core.setOutput(
'status',
requestFailed
? 'failed'
: run
? 'pending'
: process.env.NOT_FOUND_STATUS
);
return;
}
await new Promise((resolve) =>
setTimeout(resolve, Math.min(15_000, remainingMilliseconds))
);
}

View File

@@ -9,8 +9,8 @@ on:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
jobs:
changes:
@@ -38,14 +38,19 @@ jobs:
- name: Run Vitest tests with coverage
run: pnpm test:coverage
- name: Extract critical unit coverage
run: pnpm coverage:critical:extract --sha="${{ github.sha }}"
- name: Upload unit coverage artifact
if: always() && github.event_name == 'push'
if: always() && github.event_name != 'merge_group'
uses: actions/upload-artifact@v6
with:
name: unit-coverage
path: coverage/lcov.info
path: |
coverage/lcov.info
coverage/critical-unit-coverage.json
retention-days: 30
if-no-files-found: warn
if-no-files-found: error
- name: Upload coverage to Codecov
if: always()
@@ -55,3 +60,111 @@ jobs:
flags: unit
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
critical-unit-regression:
needs: test
if: >-
${{
github.event_name == 'pull_request' &&
needs.test.result == 'success' &&
(
github.base_ref == 'main' ||
github.base_ref == 'master' ||
startsWith(github.base_ref, 'dev') ||
startsWith(github.base_ref, 'core/') ||
startsWith(github.base_ref, 'desktop/')
)
}}
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 2
- name: Resolve tested base commit
id: tested-base
run: echo "sha=$(git rev-parse HEAD^1)" >> "$GITHUB_OUTPUT"
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Download head unit coverage
uses: actions/download-artifact@v7
with:
name: unit-coverage
path: temp/head-unit-coverage
- name: Find base unit coverage run
id: find-base-unit
uses: ./.github/actions/find-workflow-run
with:
workflow-id: ci-tests-unit.yaml
head-sha: ${{ steps.tested-base.outputs.sha }}
event: push
wait-seconds: 600
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download base unit coverage
id: download-base-unit
if: steps.find-base-unit.outputs.status == 'ready'
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: unit-coverage
run-id: ${{ steps.find-base-unit.outputs.run-id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: temp/base-unit-coverage
- name: Regenerate missing base unit coverage
if: steps.find-base-unit.outputs.status != 'ready' || steps.download-base-unit.outcome != 'success'
env:
BASE_SHA: ${{ steps.tested-base.outputs.sha }}
run: |
base_dir="$RUNNER_TEMP/coverage-base"
cleanup() {
git worktree remove --force "$base_dir" || true
}
trap cleanup EXIT
git worktree add "$base_dir" "$BASE_SHA"
(
cd "$base_dir"
pnpm install --frozen-lockfile
pnpm test:coverage
if [ -f scripts/critical-coverage/extractCriticalCoverage.ts ]; then
pnpm coverage:critical:extract --sha="$BASE_SHA"
fi
)
mkdir -p temp/base-unit-coverage
cp "$base_dir/coverage/lcov.info" temp/base-unit-coverage/lcov.info
if [ -f "$base_dir/coverage/critical-unit-coverage.json" ]; then
cp "$base_dir/coverage/critical-unit-coverage.json" \
temp/base-unit-coverage/
fi
- name: Prepare critical unit coverage reports
env:
BASE_SHA: ${{ steps.tested-base.outputs.sha }}
run: |
mkdir -p temp/base-critical-coverage temp/head-critical-coverage
if [ -f temp/base-unit-coverage/critical-unit-coverage.json ]; then
cp temp/base-unit-coverage/critical-unit-coverage.json \
temp/base-critical-coverage/
else
pnpm coverage:critical:extract \
--input temp/base-unit-coverage/lcov.info \
--output temp/base-critical-coverage/critical-unit-coverage.json \
--sha="$BASE_SHA"
fi
cp temp/head-unit-coverage/critical-unit-coverage.json \
temp/head-critical-coverage/
- name: Compare critical unit coverage
run: >
pnpm coverage:critical:compare
--base temp/base-critical-coverage/critical-unit-coverage.json
--head temp/head-critical-coverage/critical-unit-coverage.json

View File

@@ -19,6 +19,8 @@
"size:collect": "node scripts/size-collect.js",
"size:report": "node scripts/size-report.js",
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
"coverage:critical:compare": "tsx scripts/critical-coverage/compareCriticalCoverage.ts",
"coverage:critical:extract": "tsx scripts/critical-coverage/extractCriticalCoverage.ts",
"dev:cloud": "pnpm dev:cloud:test",
"dev:cloud:test": "cross-env DEV_SERVER_COMFYUI_URL=https://testcloud.comfy.org/ vite --config vite.config.mts",
"dev:cloud:staging": "cross-env DEV_SERVER_COMFYUI_URL=https://stagingcloud.comfy.org/ vite --config vite.config.mts",

View File

@@ -0,0 +1,494 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { createRequire } from 'node:module'
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it, onTestFinished } from 'vitest'
import { CRITICAL_COVERAGE_DIRS } from './criticalCoverageDirs'
import {
readCriticalCoverageReport,
writeCriticalCoverageReport
} from './criticalCoverageReport'
import type {
CriticalBranchCoverage,
CriticalCoverageReport
} from './criticalCoverageReport'
const require = createRequire(import.meta.url)
const TSX_CLI = require.resolve('tsx/cli')
const SCRIPT_PATH = join(
dirname(fileURLToPath(import.meta.url)),
'compareCriticalCoverage.ts'
)
interface SourceChange {
base: string
head: string
headPath?: string
}
const BASE_SHA = 'a'.repeat(40)
const HEAD_SHA = 'b'.repeat(40)
describe('compareCriticalCoverage CLI', () => {
it('passes a non-negative delta and writes the job summary', () => {
const directory = createTempDirectory()
const summaryPath = join(directory, 'summary.md')
const shared = createBranch('src/stores/a.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [shared]),
createReport(HEAD_SHA, [shared])
)
const result = runComparison(paths, { GITHUB_STEP_SUMMARY: summaryPath })
expect(result.status).toBe(0)
const summary = readFileSync(summaryPath, 'utf-8')
expect(summary).toContain('Critical Unit Coverage Gate: PASS')
expect(summary).toContain('| Covered branch delta | 0 |')
})
it('fails a negative delta and lists the regressed branch', () => {
const directory = createTempDirectory()
const covered = createBranch('src/stores/a.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [covered]),
createReport(HEAD_SHA, [{ ...covered, taken: 0, covered: false }])
)
const result = runComparison(paths)
expect(result.status).toBe(1)
expect(result.stdout).toContain('Critical Unit Coverage Gate: FAIL')
expect(result.stdout).toContain('| Covered branch delta | -1 |')
expect(result.stdout).toContain('| `src/stores/a.ts` | 1 | 0:0 | 1 | 0 |')
expect(result.stderr).toContain('regressed on 1 previously covered branch')
})
it('renders an unknown taken count as unavailable', () => {
const directory = createTempDirectory()
const covered = createBranch('src/stores/a.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [covered]),
createReport(HEAD_SHA, [{ ...covered, taken: null, covered: false }])
)
const result = runComparison(paths)
expect(result.status).toBe(1)
expect(result.stdout).toContain('| `src/stores/a.ts` | 1 | 0:0 | 1 | - |')
})
it('fails when newly covered branches offset a regression', () => {
const directory = createTempDirectory()
const regressed = createBranch('src/stores/regressed.ts', true)
const improved = createBranch('src/stores/improved.ts', false)
const paths = writeReports(
directory,
createReport(BASE_SHA, [regressed, improved]),
createReport(HEAD_SHA, [
{ ...regressed, taken: 0, covered: false },
{ ...improved, taken: 1, covered: true }
])
)
const result = runComparison(paths)
expect(result.status).toBe(1)
expect(result.stdout).toContain('| Covered branch delta | 0 |')
expect(result.stderr).toContain('regressed on 1 previously covered branch')
})
it('fails when a covered branch disappears from unchanged source', () => {
const directory = createTempDirectory()
const shared = createBranch('src/stores/shared.ts', true)
const missing = createBranch('src/stores/missing.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [shared, missing]),
createReport(HEAD_SHA, [shared])
)
const result = runComparison(paths)
expect(result.status).toBe(1)
expect(result.stderr).toContain('regressed on 1 previously covered branch')
expect(result.stdout).toContain(
'| `src/stores/missing.ts` | 1 | 0:0 | 1 | - |'
)
})
it('ignores a covered branch on a changed source line', () => {
const directory = createTempDirectory()
const file = 'src/stores/changed.ts'
const shared = createBranch('src/stores/shared.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [shared, createBranch(file, true)]),
createReport(HEAD_SHA, [shared])
)
const result = runComparison(
paths,
{},
{
[file]: {
base: 'export const changed = value ? 1 : 0\n',
head: 'export const changed = value ?? 0\n'
}
}
)
expect(result.status).toBe(0)
expect(result.stdout).toContain('| Base-only branches | 1 |')
})
it('fails when a covered branch moves and becomes uncovered', () => {
const directory = createTempDirectory()
const shared = createBranch('src/stores/shared.ts', true)
const moved = createBranch('src/stores/moved.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [shared, moved]),
createReport(HEAD_SHA, [
shared,
{
...moved,
key: 'src/stores/moved.ts:2:0:0',
line: 2,
taken: 0,
covered: false
}
])
)
const result = runComparison(
paths,
{},
{
'src/stores/moved.ts': {
base: 'export const moved = value ? 1 : 0\n',
head: '\nexport const moved = value ? 1 : 0\n'
}
}
)
expect(result.status).toBe(1)
expect(result.stderr).toContain('regressed on 1 previously covered branch')
})
it('fails when an inserted branch renumbers later LCOV blocks', () => {
const directory = createTempDirectory()
const file = 'src/stores/renumbered.ts'
const target = createBranch(file, true)
const inserted = createBranch(file, true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [target]),
createReport(HEAD_SHA, [
inserted,
{
...target,
key: `${file}:2:1:0`,
line: 2,
block: '1',
taken: 0,
covered: false
}
])
)
const result = runComparison(
paths,
{},
{
[file]: {
base: 'export const target = value ? 1 : 0\n',
head: 'export const inserted = other ? 1 : 0\nexport const target = value ? 1 : 0\n'
}
}
)
expect(result.status).toBe(1)
expect(result.stderr).toContain('regressed on 1 previously covered branch')
})
it('fails when same-line branch ordinals cross a digit boundary', () => {
const directory = createTempDirectory()
const file = 'src/stores/doubleDigit.ts'
const baseCovered = createBranch(file, true, '9')
const baseUncovered = createBranch(file, false, '10')
const headUncovered = createBranch(file, false, '10')
const headCovered = createBranch(file, true, '11')
const paths = writeReports(
directory,
createReport(BASE_SHA, [baseCovered, baseUncovered]),
createReport(HEAD_SHA, [headUncovered, headCovered])
)
const result = runComparison(paths)
expect(result.status).toBe(1)
expect(result.stderr).toContain('regressed on 1 previously covered branch')
})
it('fails when a renamed critical file loses branch coverage', () => {
const directory = createTempDirectory()
const baseFile = 'src/stores/renamedBase.ts'
const headFile = 'src/stores/renamedHead.ts'
const shared = createBranch('src/stores/shared.ts', true)
const baseBranch = createBranch(baseFile, true)
const headBranch = createBranch(headFile, false)
const paths = writeReports(
directory,
createReport(BASE_SHA, [shared, baseBranch]),
createReport(HEAD_SHA, [shared, headBranch])
)
const source = 'export const renamed = value ? 1 : 0\n'
const result = runComparison(
paths,
{},
{
[baseFile]: { base: source, head: source, headPath: headFile }
}
)
expect(result.status).toBe(1)
expect(result.stderr).toContain('regressed on 1 previously covered branch')
})
it('fails when the reports have no comparable branches', () => {
const directory = createTempDirectory()
const summaryPath = join(directory, 'summary.md')
const paths = writeReports(
directory,
createReport(BASE_SHA, [createBranch('src/stores/base.ts', true)]),
createReport(HEAD_SHA, [createBranch('src/stores/head.ts', true)])
)
const result = runComparison(paths, { GITHUB_STEP_SUMMARY: summaryPath })
expect(result.status).toBe(1)
expect(result.stderr).toContain(
'No comparable critical unit branches found (base: 1, head: 1)'
)
expect(readFileSync(summaryPath, 'utf-8')).toContain(
'Critical Unit Coverage Gate: FAIL'
)
})
it('fails when the head removes a critical directory', () => {
const directory = createTempDirectory()
const shared = createBranch('src/stores/shared.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [shared], ['src/stores', 'src/utils']),
createReport(HEAD_SHA, [shared], ['src/stores'])
)
const result = runComparison(paths)
expect(result.status).toBe(1)
expect(result.stderr).toContain(
'Critical coverage scope removed: src/utils'
)
})
it('allows the head to add a critical directory', () => {
const directory = createTempDirectory()
const shared = createBranch('src/stores/shared.ts', true)
const paths = writeReports(
directory,
createReport(BASE_SHA, [shared], ['src/stores']),
createReport(HEAD_SHA, [shared], ['src/stores', 'src/utils'])
)
expect(runComparison(paths).status).toBe(0)
})
it('reports input errors without a stack trace', () => {
const directory = createTempDirectory()
const summaryPath = join(directory, 'summary.md')
const missingPath = join(directory, 'missing.json')
const result = spawnSync(
process.execPath,
[TSX_CLI, SCRIPT_PATH, '--base', missingPath, '--head', missingPath],
{
cwd: directory,
encoding: 'utf-8',
env: { ...process.env, GITHUB_STEP_SUMMARY: summaryPath }
}
)
expect(result.status).toBe(1)
expect(result.stderr).toContain(`ENOENT: no such file or directory`)
expect(result.stderr).not.toContain(' at ')
expect(readFileSync(summaryPath, 'utf-8')).toContain(
'Critical Unit Coverage Gate: ERROR'
)
})
it('rejects unknown options', () => {
const result = spawnSync(
process.execPath,
[TSX_CLI, SCRIPT_PATH, '--unknown'],
{ encoding: 'utf-8' }
)
expect(result.status).toBe(1)
expect(result.stderr).toContain('--unknown')
expect(result.stderr).not.toContain(' at ')
})
})
function runComparison(
paths: { basePath: string; headPath: string },
env: NodeJS.ProcessEnv = {},
sourceChanges?: Record<string, SourceChange>
): ReturnType<typeof spawnSync> {
const cwd = dirname(paths.basePath)
const { baseSha, headSha } = createGitHistory(cwd, sourceChanges ?? {})
updateReportSha(paths.basePath, baseSha)
updateReportSha(paths.headPath, headSha)
return spawnSync(
process.execPath,
[TSX_CLI, SCRIPT_PATH, '--base', paths.basePath, '--head', paths.headPath],
{
cwd,
encoding: 'utf-8',
env: { ...process.env, ...env }
}
)
}
function createGitHistory(
directory: string,
sourceChanges: Record<string, SourceChange>
): { baseSha: string; headSha: string } {
execFileSync('git', ['init', '-q'], { cwd: directory })
execFileSync('git', ['config', 'user.email', 'test@example.com'], {
cwd: directory
})
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: directory })
writeFileSync(join(directory, 'fixture.txt'), 'base\n')
for (const [path, change] of Object.entries(sourceChanges)) {
const absolutePath = join(directory, path)
mkdirSync(dirname(absolutePath), { recursive: true })
writeFileSync(absolutePath, change.base)
}
execFileSync('git', ['add', 'fixture.txt', ...Object.keys(sourceChanges)], {
cwd: directory
})
execFileSync('git', ['commit', '-q', '-m', 'base'], { cwd: directory })
const baseSha = gitSha(directory)
for (const [path, change] of Object.entries(sourceChanges)) {
const headPath = change.headPath ?? path
if (headPath !== path) {
mkdirSync(dirname(join(directory, headPath)), { recursive: true })
execFileSync('git', ['mv', path, headPath], { cwd: directory })
}
writeFileSync(join(directory, headPath), change.head)
}
if (Object.keys(sourceChanges).length > 0) {
const changedPaths = Object.entries(sourceChanges).map(
([path, change]) => change.headPath ?? path
)
execFileSync('git', ['add', '-A', '--', ...changedPaths], {
cwd: directory
})
}
execFileSync('git', ['commit', '-q', '--allow-empty', '-m', 'head'], {
cwd: directory
})
return { baseSha, headSha: gitSha(directory) }
}
function updateReportSha(path: string, sha: string): void {
const report = readCriticalCoverageReport(path)
writeCriticalCoverageReport({ ...report, sha }, path)
}
function gitSha(directory: string): string {
return execFileSync('git', ['rev-parse', 'HEAD'], {
cwd: directory,
encoding: 'utf-8'
}).trim()
}
function writeReports(
directory: string,
base: CriticalCoverageReport,
head: CriticalCoverageReport
): { basePath: string; headPath: string } {
const basePath = join(directory, 'base.json')
const headPath = join(directory, 'head.json')
writeCriticalCoverageReport(base, basePath)
writeCriticalCoverageReport(head, headPath)
return { basePath, headPath }
}
function createReport(
sha: string,
branches: CriticalBranchCoverage[],
criticalDirs: readonly string[] = CRITICAL_COVERAGE_DIRS
): CriticalCoverageReport {
return {
schemaVersion: 1,
source: 'lcov',
sha,
generatedAt: '2026-07-10T00:00:00.000Z',
inputPath: 'lcov.info',
criticalDirs,
totals: {
files: new Set(branches.map(({ file }) => file)).size,
branches: branches.length,
coveredBranches: branches.filter(({ covered }) => covered).length
},
branches
}
}
function createBranch(
file: string,
covered: boolean,
block = '0'
): CriticalBranchCoverage {
return {
key: `${file}:1:${block}:0`,
file,
line: 1,
block,
branch: '0',
taken: covered ? 1 : 0,
covered
}
}
function createTempDirectory(): string {
const directory = mkdtempSync(join(tmpdir(), 'critical-coverage-cli-'))
onTestFinished(() => rmSync(directory, { recursive: true, force: true }))
return directory
}

View File

@@ -0,0 +1,162 @@
import { appendFileSync } from 'node:fs'
import { parseArgs } from 'node:util'
import { createGitLocationMapper } from './criticalCoverageGitDiff'
import {
compareCriticalCoverageReports,
readCriticalCoverageReport
} from './criticalCoverageReport'
import type { CriticalCoverageComparison } from './criticalCoverageReport'
interface Options {
base: string
head: string
}
interface GateResult {
status: 'PASS' | 'FAIL'
message: string
}
try {
main()
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
const summary = `## Critical Unit Coverage Gate: ERROR\n\n${message}`
process.stderr.write(`${message}\n`)
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`)
}
process.exitCode = 1
}
function main(): void {
const options = parseOptions(process.argv.slice(2))
const base = readCriticalCoverageReport(options.base)
const head = readCriticalCoverageReport(options.head)
const comparison = compareCriticalCoverageReports(
base,
head,
createGitLocationMapper(base.sha, head.sha)
)
const result = evaluateCoverageGate(comparison)
const summary = formatComparison(comparison, result)
process.stdout.write(`${summary}\n`)
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`)
}
if (result.status === 'FAIL') {
process.stderr.write(`${result.message}\n`)
process.exitCode = 1
}
}
function parseOptions(args: string[]): Options {
const { values: options } = parseArgs({
args,
options: {
base: { type: 'string' },
head: { type: 'string' }
},
strict: true,
allowPositionals: false
})
if (!options.base || !options.head) {
throw new Error(
'Usage: compareCriticalCoverage --base <json> --head <json>'
)
}
return {
base: options.base,
head: options.head
}
}
function evaluateCoverageGate(
comparison: CriticalCoverageComparison
): GateResult {
if (comparison.commonBranches === 0) {
return {
status: 'FAIL',
message: `No comparable critical unit branches found (base: ${comparison.baseBranches}, head: ${comparison.headBranches}).`
}
}
if (comparison.regressions.length > 0) {
const regressionCount = comparison.regressions.length
return {
status: 'FAIL',
message: `Critical unit coverage regressed on ${regressionCount} previously covered ${regressionCount === 1 ? 'branch' : 'branches'}.`
}
}
return {
status: 'PASS',
message: 'Critical branch coverage did not regress.'
}
}
function formatComparison(
comparison: CriticalCoverageComparison,
result: GateResult
): string {
const lines = [
`## Critical Unit Coverage Gate: ${result.status}`,
'',
`Base tested commit: \`${comparison.baseSha}\``,
`PR tested commit: \`${comparison.headSha}\``,
'',
'| Metric | Count |',
'|---|--:|',
`| Base critical branches | ${comparison.baseBranches} |`,
`| Head critical branches | ${comparison.headBranches} |`,
`| Comparable critical branches | ${comparison.commonBranches} |`,
`| Covered in base | ${comparison.commonCoveredBranchesInBase} |`,
`| Covered in head | ${comparison.commonCoveredBranchesInHead} |`,
`| Covered branch delta | ${formatSignedCount(comparison.coveredBranchDelta)} |`,
`| Base-only branches | ${comparison.baseOnlyBranches} |`,
`| Head-only branches | ${comparison.headOnlyBranches} |`,
`| Covered-to-uncovered branches | ${comparison.regressions.length} |`,
'',
`${result.status}: ${result.message}`
]
if (comparison.regressions.length === 0) {
return lines.join('\n')
}
lines.push('')
lines.push('| File | Line | Branch | Base | Head |')
lines.push('|---|--:|---|--:|--:|')
for (const regression of comparison.regressions.slice(0, 25)) {
lines.push(
`| \`${regression.file}\` | ${regression.line} | ${regression.block}:${regression.branch} | ${formatTaken(regression.baseTaken)} | ${formatTaken(regression.headTaken)} |`
)
}
if (comparison.regressions.length > 25) {
lines.push('')
lines.push(
`${comparison.regressions.length - 25} additional regressions omitted from this summary.`
)
}
return lines.join('\n')
}
function formatTaken(value: number | null): string {
return value === null ? '-' : String(value)
}
function formatSignedCount(value: number): string {
return value > 0 ? `+${value}` : String(value)
}

View File

@@ -0,0 +1,45 @@
export const CRITICAL_COVERAGE_DIRS = [
'src/base',
'src/composables',
'src/core',
'src/lib/litegraph/src/node',
'src/lib/litegraph/src/subgraph',
'src/lib/litegraph/src/utils',
'src/platform/assets/composables',
'src/platform/assets/mappings',
'src/platform/assets/schemas',
'src/platform/assets/services',
'src/platform/assets/utils',
'src/platform/errorCatalog',
'src/platform/keybindings',
'src/platform/missingMedia',
'src/platform/missingModel',
'src/platform/navigation',
'src/platform/nodeReplacement',
'src/platform/remote',
'src/platform/remoteConfig',
'src/platform/secrets',
'src/platform/settings',
'src/platform/workflow',
'src/platform/workspace/api',
'src/platform/workspace/auth',
'src/platform/workspace/composables',
'src/platform/workspace/stores',
'src/platform/workspace/utils',
'src/schemas',
'src/scripts',
'src/services',
'src/stores',
'src/utils',
'src/workbench/extensions/manager/composables',
'src/workbench/extensions/manager/services',
'src/workbench/extensions/manager/stores',
'src/workbench/extensions/manager/utils',
'src/workbench/utils'
] as const
export function isCriticalCoveragePath(filePath: string): boolean {
return CRITICAL_COVERAGE_DIRS.some(
(dir) => filePath === dir || filePath.startsWith(`${dir}/`)
)
}

View File

@@ -0,0 +1,187 @@
import { execFileSync } from 'node:child_process'
import { CRITICAL_COVERAGE_DIRS } from './criticalCoverageDirs'
import { isGitCommitSha } from './criticalCoverageReport'
import type {
CriticalCoverageLocation,
CriticalCoverageLocationMapper
} from './criticalCoverageReport'
interface DiffFile {
headPath: string | null
hunks: DiffHunk[]
}
interface DiffHunk {
baseStart: number
baseCount: number
headCount: number
}
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+\d+(?:,(\d+))? @@/
const MAX_GIT_OUTPUT_BYTES = 100 * 1024 * 1024
export function createGitLocationMapper(
baseSha: string,
headSha: string,
cwd = process.cwd()
): CriticalCoverageLocationMapper {
if (!isGitCommitSha(baseSha) || !isGitCommitSha(headSha)) {
throw new Error(
'Critical coverage comparison requires full Git commit SHAs'
)
}
const files = readDiffFiles(baseSha, headSha, cwd)
const diff = runGitDiff(baseSha, headSha, cwd, [
'--unified=0',
'--no-color',
'--no-ext-diff'
])
addDiffHunks(files, diff)
return (file, line) => mapBaseLocation(files.get(file), file, line)
}
function readDiffFiles(
baseSha: string,
headSha: string,
cwd: string
): Map<string, DiffFile> {
const output = runGitDiff(baseSha, headSha, cwd, ['--name-status', '-z'])
const tokens = output.split('\0')
const files = new Map<string, DiffFile>()
for (let index = 0; index < tokens.length; ) {
const status = tokens[index++]
if (!status) {
continue
}
const basePath = tokens[index++]
if (!basePath) {
break
}
if (status.startsWith('R')) {
const headPath = tokens[index++]
if (!headPath) {
break
}
files.set(basePath, { headPath, hunks: [] })
continue
}
if (status === 'A') {
continue
}
files.set(basePath, {
headPath: status === 'D' ? null : basePath,
hunks: []
})
}
return files
}
function runGitDiff(
baseSha: string,
headSha: string,
cwd: string,
options: string[]
): string {
return execFileSync(
'git',
[
'-c',
'core.quotePath=false',
'diff',
'--find-renames',
...options,
baseSha,
headSha,
'--',
...CRITICAL_COVERAGE_DIRS
],
{ cwd, encoding: 'utf-8', maxBuffer: MAX_GIT_OUTPUT_BYTES }
)
}
function addDiffHunks(files: Map<string, DiffFile>, diff: string): void {
let basePath: string | null = null
for (const line of diff.split('\n')) {
if (line.startsWith('diff --git ')) {
basePath = null
continue
}
if (line.startsWith('--- ')) {
basePath = parseDiffPath(line.slice(4), 'a/')
continue
}
const match = HUNK_HEADER.exec(line)
const file = basePath ? files.get(basePath) : undefined
if (!match || !file) {
continue
}
file.hunks.push({
baseStart: Number(match[1]),
baseCount: Number(match[2] ?? 1),
headCount: Number(match[3] ?? 1)
})
}
}
function parseDiffPath(path: string, prefix: string): string | null {
return path.startsWith(prefix) ? path.slice(prefix.length) : null
}
function mapBaseLocation(
file: DiffFile | undefined,
basePath: string,
line: number
): CriticalCoverageLocation | null {
if (!file) {
return { file: basePath, line }
}
if (file.headPath === null) {
return null
}
const mappedLine = mapBaseLine(file.hunks, line)
return mappedLine === null ? null : { file: file.headPath, line: mappedLine }
}
function mapBaseLine(hunks: DiffHunk[], line: number): number | null {
let offset = 0
for (const hunk of hunks) {
if (hunk.baseCount === 0) {
if (line <= hunk.baseStart) {
return line + offset
}
offset += hunk.headCount
continue
}
if (line < hunk.baseStart) {
return line + offset
}
if (line < hunk.baseStart + hunk.baseCount) {
return null
}
offset += hunk.headCount - hunk.baseCount
}
return line + offset
}

View File

@@ -0,0 +1,404 @@
import { spawnSync } from 'node:child_process'
import { createRequire } from 'node:module'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it, onTestFinished } from 'vitest'
import { CRITICAL_COVERAGE_DIRS } from './criticalCoverageDirs'
import {
compareCriticalCoverageReports,
createCriticalCoverageReport,
readCriticalCoverageReport,
writeCriticalCoverageReport
} from './criticalCoverageReport'
import type {
CriticalBranchCoverage,
CriticalCoverageReport
} from './criticalCoverageReport'
const GENERATED_AT = '2026-07-10T00:00:00.000Z'
const BASE_SHA = 'a'.repeat(40)
const HEAD_SHA = 'b'.repeat(40)
const require = createRequire(import.meta.url)
const TSX_CLI = require.resolve('tsx/cli')
const EXTRACT_SCRIPT_PATH = join(
dirname(fileURLToPath(import.meta.url)),
'extractCriticalCoverage.ts'
)
interface InvalidReportCase {
name: string
corrupt(report: CriticalCoverageReport): unknown
}
const INVALID_REPORT_CASES: InvalidReportCase[] = [
{
name: 'invalid commit SHA',
corrupt(report) {
return { ...report, sha: '--output=/tmp/coverage' }
}
},
{
name: 'a branch outside the recorded critical directories',
corrupt(report) {
return { ...report, criticalDirs: ['src/components'] }
}
},
{
name: 'fractional totals',
corrupt(report) {
return {
...report,
totals: { ...report.totals, files: 0.5 }
}
}
},
{
name: 'inconsistent totals',
corrupt(report) {
return {
...report,
totals: { ...report.totals, branches: report.totals.branches + 1 }
}
}
},
{
name: 'arbitrary branch keys',
corrupt(report) {
return {
...report,
branches: report.branches.map((branch) => ({
...branch,
key: 'arbitrary'
}))
}
}
},
{
name: 'duplicate branch keys',
corrupt(report) {
return {
...report,
totals: {
...report.totals,
branches: report.totals.branches * 2,
coveredBranches: report.totals.coveredBranches * 2
},
branches: [...report.branches, ...report.branches]
}
}
},
{
name: 'inconsistent branch coverage',
corrupt(report) {
return {
...report,
totals: { ...report.totals, coveredBranches: 0 },
branches: report.branches.map((branch) => ({
...branch,
covered: false
}))
}
}
}
]
describe('createCriticalCoverageReport', () => {
it('extracts critical branches and calculates coverage totals', () => {
const fixture = createLcovFixture(`
SF:src/stores/queueStore.ts
BRDA:10,0,0,3
BRDA:10,0,1,0
BRDA:11,0,0,-
BRDA:0,0,2,1
BRDA:12,invalid,0,1
BRDA:13,0,0,1.5
end_of_record
SF:src/components/QueuePanel.vue
BRDA:20,0,0,4
end_of_record
`)
const report = createCriticalCoverageReport({
inputPath: fixture.inputPath,
sha: HEAD_SHA,
generatedAt: GENERATED_AT,
cwd: fixture.directory
})
expect(report.totals).toEqual({
files: 1,
branches: 3,
coveredBranches: 1
})
expect(
report.branches.map(({ key, taken, covered }) => ({
key,
taken,
covered
}))
).toEqual([
{
key: 'src/stores/queueStore.ts:10:0:0',
taken: 3,
covered: true
},
{
key: 'src/stores/queueStore.ts:10:0:1',
taken: 0,
covered: false
},
{
key: 'src/stores/queueStore.ts:11:0:0',
taken: null,
covered: false
}
])
})
it('merges duplicate branch records into a consistent coverage state', () => {
const fixture = createLcovFixture(`
SF:src/stores/queueStore.ts
BRDA:10,0,0,-
BRDA:10,0,0,2
end_of_record
`)
const report = createCriticalCoverageReport({
inputPath: fixture.inputPath,
sha: HEAD_SHA,
generatedAt: GENERATED_AT,
cwd: fixture.directory
})
expect(report.branches).toEqual([
expect.objectContaining({ taken: 2, covered: true })
])
})
it('rejects an empty LCOV taken value', () => {
const fixture = createLcovFixture(`
SF:src/stores/queueStore.ts
BRDA:10,0,0,
end_of_record
`)
const report = createCriticalCoverageReport({
inputPath: fixture.inputPath,
sha: HEAD_SHA,
generatedAt: GENERATED_AT,
cwd: fixture.directory
})
expect(report.branches).toEqual([])
})
it('reports extraction errors without a stack trace', () => {
const directory = createTempDirectory()
const summaryPath = join(directory, 'summary.md')
const missingPath = join(directory, 'missing.info')
const result = spawnSync(
process.execPath,
[TSX_CLI, EXTRACT_SCRIPT_PATH, '--input', missingPath, '--sha', HEAD_SHA],
{
encoding: 'utf-8',
env: { ...process.env, GITHUB_STEP_SUMMARY: summaryPath }
}
)
expect(result.status).toBe(1)
expect(result.stderr).toContain(`ENOENT: no such file or directory`)
expect(result.stderr).not.toContain(' at ')
expect(readFileSync(summaryPath, 'utf-8')).toContain(
'Critical Unit Coverage Extraction: ERROR'
)
})
it.for([
{
name: 'an unknown option',
args: ['--unknown'],
expected: '--unknown'
},
{
name: 'an incomplete option',
args: ['--input', `--sha=${HEAD_SHA}`],
expected: '--input'
}
])('rejects $name', ({ args, expected }) => {
const result = spawnSync(
process.execPath,
[TSX_CLI, EXTRACT_SCRIPT_PATH, ...args],
{ encoding: 'utf-8' }
)
expect(result.status).toBe(1)
expect(result.stderr).toContain(expected)
expect(result.stderr).not.toContain(' at ')
})
})
describe('readCriticalCoverageReport', () => {
it('accepts a report with its own valid critical-directory scope', () => {
const directory = createTempDirectory()
const inputPath = join(directory, 'coverage.json')
const report = {
...createReport(HEAD_SHA, [
createBranch('src/stores/queueStore.ts', true)
]),
criticalDirs: ['src/stores']
}
writeFileSync(inputPath, JSON.stringify(report))
expect(readCriticalCoverageReport(inputPath)).toEqual(report)
})
it('round-trips negative LCOV branch counts', () => {
const fixture = createLcovFixture(`
SF:src/utils/linkFixer.ts
BRDA:449,72,1,-2
end_of_record
`)
const outputPath = join(fixture.directory, 'coverage.json')
const report = createCriticalCoverageReport({
inputPath: fixture.inputPath,
sha: HEAD_SHA,
generatedAt: GENERATED_AT,
cwd: fixture.directory
})
writeCriticalCoverageReport(report, outputPath)
expect(readCriticalCoverageReport(outputPath)).toEqual(report)
})
it('round-trips branch ordinals reused on different lines', () => {
const fixture = createLcovFixture(`
SF:src/stores/queueStore.ts
BRDA:10,0,0,1
BRDA:11,0,0,1
end_of_record
`)
const outputPath = join(fixture.directory, 'coverage.json')
const report = createCriticalCoverageReport({
inputPath: fixture.inputPath,
sha: HEAD_SHA,
generatedAt: GENERATED_AT,
cwd: fixture.directory
})
writeCriticalCoverageReport(report, outputPath)
expect(readCriticalCoverageReport(outputPath)).toEqual(report)
})
it('rejects malformed artifacts', () => {
const directory = createTempDirectory()
const inputPath = join(directory, 'coverage.json')
writeFileSync(
inputPath,
JSON.stringify({
...createReport(HEAD_SHA, []),
branches: [{ key: 'invalid' }]
})
)
expect(() => readCriticalCoverageReport(inputPath)).toThrow(
`Invalid critical coverage report: ${inputPath}`
)
})
it.for(INVALID_REPORT_CASES)('rejects $name', ({ corrupt }) => {
const directory = createTempDirectory()
const inputPath = join(directory, 'coverage.json')
const report = createReport(HEAD_SHA, [
createBranch('src/stores/queueStore.ts', true)
])
writeFileSync(inputPath, JSON.stringify(corrupt(report)))
expect(() => readCriticalCoverageReport(inputPath)).toThrow(
`Invalid critical coverage report: ${inputPath}`
)
})
})
describe('compareCriticalCoverageReports', () => {
it('reports covered base-only branches as regressions', () => {
const shared = createBranch('src/stores/shared.ts', true)
const base = createReport(BASE_SHA, [
shared,
createBranch('src/stores/base-only.ts', true)
])
const head = createReport(HEAD_SHA, [
shared,
createBranch('src/stores/head-only.ts', false)
])
expect(compareCriticalCoverageReports(base, head)).toMatchObject({
commonBranches: 1,
baseOnlyBranches: 1,
headOnlyBranches: 1,
commonCoveredBranchesInBase: 1,
commonCoveredBranchesInHead: 1,
coveredBranchDelta: 0,
regressions: [
expect.objectContaining({
file: 'src/stores/base-only.ts',
baseTaken: 1,
headTaken: null
})
]
})
})
})
function createLcovFixture(lcov: string): {
directory: string
inputPath: string
} {
const directory = createTempDirectory()
const inputPath = join(directory, 'lcov.info')
writeFileSync(inputPath, lcov.trimStart())
return { directory, inputPath }
}
function createTempDirectory(): string {
const directory = mkdtempSync(join(tmpdir(), 'critical-coverage-'))
onTestFinished(() => rmSync(directory, { recursive: true, force: true }))
return directory
}
function createReport(
sha: string,
branches: CriticalBranchCoverage[]
): CriticalCoverageReport {
return {
schemaVersion: 1,
source: 'lcov',
sha,
generatedAt: GENERATED_AT,
inputPath: 'lcov.info',
criticalDirs: CRITICAL_COVERAGE_DIRS,
totals: {
files: new Set(branches.map(({ file }) => file)).size,
branches: branches.length,
coveredBranches: branches.filter(({ covered }) => covered).length
},
branches
}
}
function createBranch(file: string, covered: boolean): CriticalBranchCoverage {
return {
key: `${file}:1:0:0`,
file,
line: 1,
block: '0',
branch: '0',
taken: covered ? 1 : 0,
covered
}
}

View File

@@ -0,0 +1,485 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, isAbsolute, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
import {
CRITICAL_COVERAGE_DIRS,
isCriticalCoveragePath
} from './criticalCoverageDirs'
export interface CriticalBranchCoverage {
key: string
file: string
line: number
block: string
branch: string
taken: number | null
covered: boolean
}
export interface CriticalCoverageReport {
schemaVersion: 1
source: 'lcov'
sha: string
generatedAt: string
inputPath: string
criticalDirs: readonly string[]
totals: {
files: number
branches: number
coveredBranches: number
}
branches: CriticalBranchCoverage[]
}
export interface CriticalCoverageRegression extends CriticalBranchCoverage {
baseTaken: number | null
headTaken: number | null
}
export interface CriticalCoverageComparison {
baseSha: string
headSha: string
baseBranches: number
headBranches: number
commonBranches: number
baseOnlyBranches: number
headOnlyBranches: number
commonCoveredBranchesInBase: number
commonCoveredBranchesInHead: number
coveredBranchDelta: number
regressions: CriticalCoverageRegression[]
}
export interface CriticalCoverageLocation {
file: string
line: number
}
export type CriticalCoverageLocationMapper = (
file: string,
line: number
) => CriticalCoverageLocation | null
interface CreateReportOptions {
inputPath: string
sha: string
generatedAt?: string
cwd?: string
}
interface IndexedBranch {
branch: CriticalBranchCoverage
mappedLocation: CriticalCoverageLocation | null
}
export function createCriticalCoverageReport({
inputPath,
sha,
generatedAt = new Date().toISOString(),
cwd = process.cwd()
}: CreateReportOptions): CriticalCoverageReport {
if (!isGitCommitSha(sha)) {
throw new Error(`Invalid Git commit SHA: ${sha}`)
}
const lcov = readFileSync(inputPath, 'utf-8')
const branches = parseCriticalBranches(lcov, cwd)
const files = new Set(branches.map((branch) => branch.file))
const coveredBranches = branches.filter((branch) => branch.covered).length
return {
schemaVersion: 1,
source: 'lcov',
sha,
generatedAt,
inputPath,
criticalDirs: CRITICAL_COVERAGE_DIRS,
totals: {
files: files.size,
branches: branches.length,
coveredBranches
},
branches
}
}
export function writeCriticalCoverageReport(
report: CriticalCoverageReport,
outputPath: string
): void {
mkdirSync(dirname(outputPath), { recursive: true })
writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`)
}
export function readCriticalCoverageReport(
inputPath: string
): CriticalCoverageReport {
const parsed: unknown = JSON.parse(readFileSync(inputPath, 'utf-8'))
if (!isCriticalCoverageReport(parsed)) {
throw new Error(`Invalid critical coverage report: ${inputPath}`)
}
return parsed
}
export function compareCriticalCoverageReports(
base: CriticalCoverageReport,
head: CriticalCoverageReport,
mapBaseLocation: CriticalCoverageLocationMapper = identityLocationMapper
): CriticalCoverageComparison {
assertCriticalCoverageScopePreserved(base.criticalDirs, head.criticalDirs)
const baseBranches = indexBranches(base.branches, mapBaseLocation, 'base')
const headBranches = indexBranches(
head.branches,
identityLocationMapper,
'head'
)
const regressions: CriticalCoverageRegression[] = []
let commonBranches = 0
let commonCoveredBranchesInBase = 0
let commonCoveredBranchesInHead = 0
let baseOnlyBranches = 0
for (const [key, indexedBaseBranch] of baseBranches) {
const indexedHeadBranch = headBranches.get(key)
const baseBranch = indexedBaseBranch.branch
if (!indexedHeadBranch) {
baseOnlyBranches++
if (baseBranch.covered && indexedBaseBranch.mappedLocation) {
regressions.push({
...baseBranch,
file: indexedBaseBranch.mappedLocation.file,
line: indexedBaseBranch.mappedLocation.line,
taken: null,
covered: false,
baseTaken: baseBranch.taken,
headTaken: null
})
}
continue
}
const headBranch = indexedHeadBranch.branch
commonBranches++
if (baseBranch.covered) {
commonCoveredBranchesInBase++
}
if (headBranch.covered) {
commonCoveredBranchesInHead++
}
if (baseBranch.covered && !headBranch.covered) {
regressions.push({
...headBranch,
baseTaken: baseBranch.taken,
headTaken: headBranch.taken
})
}
}
return {
baseSha: base.sha,
headSha: head.sha,
baseBranches: base.branches.length,
headBranches: head.branches.length,
commonBranches,
baseOnlyBranches,
headOnlyBranches: [...headBranches.keys()].filter(
(key) => !baseBranches.has(key)
).length,
commonCoveredBranchesInBase,
commonCoveredBranchesInHead,
coveredBranchDelta:
commonCoveredBranchesInHead - commonCoveredBranchesInBase,
regressions: regressions.sort(compareBranches)
}
}
function parseCriticalBranches(
lcov: string,
cwd: string
): CriticalBranchCoverage[] {
let currentFile = ''
const branches = new Map<string, CriticalBranchCoverage>()
for (const line of lcov.split('\n')) {
if (line.startsWith('SF:')) {
currentFile = normalizeCoveragePath(line.slice(3), cwd)
continue
}
if (!line.startsWith('BRDA:') || !isCriticalCoveragePath(currentFile)) {
continue
}
const branch = parseBranchData(currentFile, line.slice(5))
if (!branch) {
continue
}
const existing = branches.get(branch.key)
if (!existing) {
branches.set(branch.key, branch)
continue
}
branches.set(branch.key, mergeBranchCoverage(existing, branch))
}
return [...branches.values()].sort(compareBranches)
}
function parseBranchData(
file: string,
data: string
): CriticalBranchCoverage | null {
const [lineValue, block, branch, takenValue] = data.split(',')
const line = Number(lineValue)
const taken = takenValue === '-' ? null : Number(takenValue)
if (
!Number.isInteger(line) ||
line <= 0 ||
!isCoverageOrdinal(block) ||
!isCoverageOrdinal(branch) ||
takenValue === undefined ||
takenValue.trim().length === 0 ||
(taken !== null && !Number.isInteger(taken))
) {
return null
}
const covered = taken !== null && taken > 0
const key = `${file}:${line}:${block}:${branch}`
return {
key,
file,
line,
block,
branch,
taken,
covered
}
}
function mergeBranchCoverage(
left: CriticalBranchCoverage,
right: CriticalBranchCoverage
): CriticalBranchCoverage {
const taken =
left.taken === null && right.taken === null
? null
: (left.taken ?? 0) + (right.taken ?? 0)
return {
...left,
taken,
covered: taken !== null && taken > 0
}
}
function compareBranches(
left: CriticalBranchCoverage,
right: CriticalBranchCoverage
): number {
return (
left.file.localeCompare(right.file) ||
left.line - right.line ||
Number(left.block) - Number(right.block) ||
Number(left.branch) - Number(right.branch)
)
}
function indexBranches(
branches: CriticalBranchCoverage[],
mapLocation: CriticalCoverageLocationMapper,
unmappedPrefix: string
): Map<string, IndexedBranch> {
const indexed = new Map<string, IndexedBranch>()
const occurrences = new Map<string, number>()
for (const branch of [...branches].sort(compareBranches)) {
const sourceLine = `${branch.file}:${branch.line}`
const occurrence = occurrences.get(sourceLine) ?? 0
const mappedLocation = mapLocation(branch.file, branch.line)
const identity =
mappedLocation === null
? `${unmappedPrefix}:${branch.key}`
: `${mappedLocation.file}:${mappedLocation.line}:${occurrence}`
occurrences.set(sourceLine, occurrence + 1)
indexed.set(identity, { branch, mappedLocation })
}
return indexed
}
function assertCriticalCoverageScopePreserved(
baseDirs: readonly string[],
headDirs: readonly string[]
): void {
const headScope = new Set(headDirs)
const removedDirs = baseDirs.filter((dir) => !headScope.has(dir))
if (removedDirs.length > 0) {
throw new Error(
`Critical coverage scope removed: ${removedDirs.join(', ')}`
)
}
}
function identityLocationMapper(
file: string,
line: number
): CriticalCoverageLocation {
return { file, line }
}
function normalizeCoveragePath(filePath: string, cwd: string): string {
const decodedPath = filePath.startsWith('file://')
? fileURLToPath(filePath)
: filePath
const relativePath = isAbsolute(decodedPath)
? relative(cwd, decodedPath)
: decodedPath
const normalizedPath = relativePath.replace(/\\/g, '/').replace(/^\.\//, '')
if (!normalizedPath.startsWith('../')) {
return normalizedPath
}
const srcIndex = normalizedPath.indexOf('/src/')
return srcIndex === -1 ? normalizedPath : normalizedPath.slice(srcIndex + 1)
}
function isCriticalCoverageReport(
value: unknown
): value is CriticalCoverageReport {
if (!isRecord(value)) {
return false
}
const totals = value.totals
const branches = value.branches
const criticalDirs = value.criticalDirs
if (
value.schemaVersion !== 1 ||
value.source !== 'lcov' ||
!isGitCommitSha(value.sha) ||
typeof value.generatedAt !== 'string' ||
typeof value.inputPath !== 'string' ||
!isCriticalCoverageDirs(criticalDirs) ||
!isRecord(totals) ||
!isNonNegativeInteger(totals.files) ||
!isNonNegativeInteger(totals.branches) ||
!isNonNegativeInteger(totals.coveredBranches) ||
!Array.isArray(branches) ||
!branches.every((branch) => isCriticalBranchCoverage(branch, criticalDirs))
) {
return false
}
const keys = new Set(branches.map(({ key }) => key))
const files = new Set(branches.map(({ file }) => file))
const coveredBranches = branches.filter(({ covered }) => covered).length
return (
keys.size === branches.length &&
totals.files === files.size &&
totals.branches === branches.length &&
totals.coveredBranches === coveredBranches
)
}
function isCriticalBranchCoverage(
value: unknown,
criticalDirs: readonly string[]
): value is CriticalBranchCoverage {
if (!isRecord(value)) {
return false
}
const taken = value.taken
if (
typeof value.key !== 'string' ||
typeof value.file !== 'string' ||
!isCoveragePathInDirs(value.file, criticalDirs) ||
!isNonNegativeInteger(value.line) ||
value.line === 0 ||
!isCoverageOrdinal(value.block) ||
!isCoverageOrdinal(value.branch) ||
(taken !== null &&
(typeof taken !== 'number' || !Number.isInteger(taken))) ||
typeof value.covered !== 'boolean'
) {
return false
}
return (
value.key ===
`${value.file}:${value.line}:${value.block}:${value.branch}` &&
value.covered === (typeof taken === 'number' && taken > 0)
)
}
function isCriticalCoverageDirs(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every(
(dir) =>
typeof dir === 'string' &&
dir.length > 0 &&
dir === dir.trim() &&
!dir.startsWith('/') &&
!dir.endsWith('/') &&
!dir.includes('\\') &&
!dir.split('/').includes('..')
) &&
new Set(value).size === value.length
)
}
function isCoveragePathInDirs(
filePath: string,
criticalDirs: readonly string[]
): boolean {
return criticalDirs.some(
(dir) => filePath === dir || filePath.startsWith(`${dir}/`)
)
}
function isNonNegativeInteger(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value) && value >= 0
}
export function isGitCommitSha(value: unknown): value is string {
return (
typeof value === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value)
)
}
function isCoverageOrdinal(value: unknown): value is string {
return (
typeof value === 'string' &&
/^\d+$/.test(value) &&
Number.isSafeInteger(Number(value))
)
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}

View File

@@ -0,0 +1,65 @@
import { appendFileSync } from 'node:fs'
import { parseArgs } from 'node:util'
import {
createCriticalCoverageReport,
writeCriticalCoverageReport
} from './criticalCoverageReport'
interface Options {
input: string
output: string
sha: string
}
try {
main()
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
const summary = `## Critical Unit Coverage Extraction: ERROR\n\n${message}`
process.stderr.write(`${message}\n`)
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`)
}
process.exitCode = 1
}
function main(): void {
const options = parseOptions(process.argv.slice(2))
const report = createCriticalCoverageReport({
inputPath: options.input,
sha: options.sha
})
writeCriticalCoverageReport(report, options.output)
process.stdout.write(
[
`Critical coverage branches: ${report.totals.coveredBranches}/${report.totals.branches}`,
`Critical coverage files: ${report.totals.files}`,
`Wrote ${options.output}`
].join('\n') + '\n'
)
}
function parseOptions(args: string[]): Options {
const { values } = parseArgs({
args,
options: {
input: { type: 'string' },
output: { type: 'string' },
sha: { type: 'string' }
},
strict: true,
allowPositionals: false
})
return {
input: values.input ?? 'coverage/lcov.info',
output: values.output ?? 'coverage/critical-unit-coverage.json',
sha: values.sha ?? process.env.GITHUB_SHA ?? ''
}
}

View File

@@ -18,6 +18,7 @@ import { createHtmlPlugin } from 'vite-plugin-html'
import vueDevTools from 'vite-plugin-vue-devtools'
import { comfyAPIPlugin } from './build/plugins'
import { CRITICAL_COVERAGE_DIRS } from './scripts/critical-coverage/criticalCoverageDirs'
dotenvConfig()
@@ -30,46 +31,6 @@ const DISABLE_TEMPLATES_PROXY = process.env.DISABLE_TEMPLATES_PROXY === 'true'
const GENERATE_SOURCEMAP = process.env.GENERATE_SOURCEMAP !== 'false'
const IS_STORYBOOK = process.env.npm_lifecycle_event === 'storybook'
const CRITICAL_COVERAGE_DIRS = [
'src/base',
'src/composables',
'src/core',
'src/lib/litegraph/src/node',
'src/lib/litegraph/src/subgraph',
'src/lib/litegraph/src/utils',
'src/platform/assets/composables',
'src/platform/assets/mappings',
'src/platform/assets/schemas',
'src/platform/assets/services',
'src/platform/assets/utils',
'src/platform/errorCatalog',
'src/platform/keybindings',
'src/platform/missingMedia',
'src/platform/missingModel',
'src/platform/navigation',
'src/platform/nodeReplacement',
'src/platform/remote',
'src/platform/remoteConfig',
'src/platform/secrets',
'src/platform/settings',
'src/platform/workflow',
'src/platform/workspace/api',
'src/platform/workspace/auth',
'src/platform/workspace/composables',
'src/platform/workspace/stores',
'src/platform/workspace/utils',
'src/schemas',
'src/scripts',
'src/services',
'src/stores',
'src/utils',
'src/workbench/extensions/manager/composables',
'src/workbench/extensions/manager/services',
'src/workbench/extensions/manager/stores',
'src/workbench/extensions/manager/utils',
'src/workbench/utils'
]
// A single glob key so vitest aggregates all critical dirs into one
// thresholds bucket instead of one bucket per glob
const CRITICAL_COVERAGE_GLOB = `{${CRITICAL_COVERAGE_DIRS.join(',')}}/**/*.{ts,vue}`