fix(ci): align local verify with PR policy

Fail PR and release quality runs when the impact policy blocks the diff, and accept root-runtime regression coverage across service and utility seams.

Tested: bun run check:policy
Confidence: high
Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-07-10 20:37:43 +08:00
parent 435e4ccc9f
commit 8cada04d10
4 changed files with 90 additions and 14 deletions

View File

@ -171,6 +171,19 @@ describe('evaluateChangePolicy', () => {
expect(covered.missingTestSignals).toEqual([])
})
test('accepts root runtime regression tests across service and utility seams', () => {
const result = evaluateChangePolicy(
[
'src/utils/messages.ts',
'src/services/api/streamRetry.test.ts',
],
['allow-cli-core-change'],
)
expect(result.missingTestSignals).toEqual([])
expect(result.blocked).toBe(false)
})
test('blocks coverage baseline and threshold changes without maintainer override', () => {
const result = evaluateChangePolicy([
'scripts/quality-gate/coverage-baseline.json',

View File

@ -249,15 +249,9 @@ function missingTestSignals(files: string[]) {
const desktopProd = changedProductionFiles(files, (file) => file.startsWith('desktop/src/'))
const serverProd = changedProductionFiles(files, (file) => file.startsWith('src/server/'))
const adapterProd = changedProductionFiles(files, (file) => file.startsWith('adapters/'))
const agentRuntimeProd = changedProductionFiles(files, (file) => (
file.startsWith('src/tools/') ||
file.startsWith('src/utils/')
))
const rootRuntimeProd = changedProductionFiles(files, (file) => (
file.startsWith('src/') &&
!file.startsWith('src/server/') &&
!file.startsWith('src/tools/') &&
!file.startsWith('src/utils/')
!file.startsWith('src/server/')
))
if (desktopProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('desktop/src/'))) {
@ -269,14 +263,9 @@ function missingTestSignals(files: string[]) {
if (adapterProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('adapters/'))) {
signals.push('Adapter product files changed without an adapter test file in the PR.')
}
if (agentRuntimeProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('src/tools/') || file.startsWith('src/utils/'))) {
signals.push('Agent/runtime product files changed without a tools/utils test file in the PR.')
}
if (rootRuntimeProd.length > 0 && !hasMatchingTest(files, (file) => (
file.startsWith('src/') &&
!file.startsWith('src/server/') &&
!file.startsWith('src/tools/') &&
!file.startsWith('src/utils/')
!file.startsWith('src/server/')
))) {
signals.push('Root runtime product files changed without a matching root runtime test file in the PR.')
}

View File

@ -433,6 +433,52 @@ describe('runQualityGate', () => {
}
})
test('fails PR mode when the impact report is blocked by policy', async () => {
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
const lanes: LaneDefinition[] = [
{
id: 'impact-report',
title: 'Impact report',
description: 'Writes a blocked policy decision',
kind: 'command',
command: ['bash', '-lc', [
'printf "%s\\n"',
'"# PR impact report"',
'""',
'"Changed files: 1"',
'"Areas: cli-core"',
'"Labels: none"',
'"Blocked: yes"',
'"Blocking reasons:"',
'"- CLI core changes require maintainer approval."',
'""',
'"## Required local checks"',
'"- bun run check:server"',
].join(' ')],
requiredForModes: ['pr'],
},
]
try {
const { report } = await runQualityGateLanes({
mode: 'pr',
dryRun: false,
allowLive: false,
baselineTargets: [],
rootDir: process.cwd(),
artifactsDir,
runId: 'blocked-impact-test',
}, lanes)
expect(report.impact?.blocked).toBe(true)
expect(report.results[0].status).toBe('failed')
expect(report.results[0].error).toContain('change policy blocked')
expect(report.summary.failed).toBe(1)
} finally {
rmSync(artifactsDir, { recursive: true, force: true })
}
})
test('release mode treats skipped live lanes as failures', async () => {
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
const lanes: LaneDefinition[] = [

View File

@ -432,6 +432,33 @@ function enforceReleaseLiveLanes(
})
}
function enforceBlockedImpact(
options: QualityGateOptions,
results: LaneResult[],
) {
if (options.dryRun || (options.mode !== 'pr' && options.mode !== 'release')) {
return results
}
const impact = parseImpactSummary(results)
if (!impact?.blocked) {
return results
}
return results.map((result) => {
if (result.id !== 'impact-report' || result.status === 'failed') {
return result
}
return {
...result,
status: 'failed' as const,
exitCode: result.exitCode === 0 ? 1 : result.exitCode,
error: 'Local change policy blocked this diff; inspect the impact report and use maintainer overrides only when explicitly approved.',
}
})
}
export async function runQualityGate(options: QualityGateOptions) {
return runQualityGateLanes(options, lanesForMode(options.mode, options.baselineTargets))
}
@ -454,7 +481,8 @@ export async function runQualityGateLanes(
const result = await executeLane(lane, runOptions)
rawResults.push(withLaneMetadata(lane, result))
}
const results = enforceReleaseLiveLanes(options, selectedLanes, rawResults)
const releaseResults = enforceReleaseLiveLanes(options, selectedLanes, rawResults)
const results = enforceBlockedImpact(options, releaseResults)
const report: QualityGateReport = {
schemaVersion: 1,