Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 5b4845d7b7 test(ci): clarify incomplete-baseline fail-closed coverage
Keep the comparator fail-closed on truncated exact-base TAP summaries
and re-request Codex review on the current head.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-08-05 10:54:29 +00:00
Cursor Agentand陈大猫 b17c885515 fix(ci): fail closed on incomplete baseline TAP summaries
Require a complete exact-base summary before accepting a quantitative
clean candidate, so a truncated red baseline cannot be bypassed by
deleting crashing coverage.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-08-05 10:47:34 +00:00
Cursor Agentand陈大猫 69ecc6a6ce fix(ci): keep comparator comments ASCII-only
Replace the non-ASCII arrow in the red-to-green comment so the file
stays consistent with the repo ASCII convention.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-08-05 10:41:58 +00:00
Cursor Agentand陈大猫 9c6bd72768 fix(ci): require baseline failures to be fixed by name
Address Codex P1: a red→green candidate must keep each failing test
title as a success, so deleting the failing test and adding an unrelated
passer cannot pass the quantitative clean gate.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-08-05 10:37:51 +00:00
Cursor Agentand陈大猫 dabe1397ee fix(ci): stop green Codex fixes failing baseline name matching
Accept exit-0 candidates when quantitative coverage does not shrink,
even if individual TAP success titles were renamed. Keep rejecting
true coverage loss, upload baseline test logs, and surface comparison
details on rejected verification runs.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-08-05 10:27:09 +00:00
4 changed files with 158 additions and 45 deletions

No files matched your search

+22
View File
@@ -2093,6 +2093,7 @@ jobs:
".cursor-runtime/followup-pending-snapshots.json",
".cursor-runtime/followup-verify-result.json",
".cursor-runtime/followup-test-comparison.json",
".cursor-runtime/followup-base-tests.log",
".cursor-runtime/followup-candidate-lint.log",
".cursor-runtime/followup-candidate-tests.log",
".cursor-runtime/followup-candidate-build.log",
@@ -2114,6 +2115,7 @@ jobs:
.cursor-runtime/followup-pending-snapshots.json
.cursor-runtime/followup-verify-result.json
.cursor-runtime/followup-test-comparison.json
.cursor-runtime/followup-base-tests.log
.cursor-runtime/followup-candidate-lint.log
.cursor-runtime/followup-candidate-tests.log
.cursor-runtime/followup-candidate-build.log
@@ -3379,6 +3381,7 @@ jobs:
".cursor-runtime/implement-pr-body.md",
".cursor-runtime/verify-result.json",
".cursor-runtime/test-comparison.json",
".cursor-runtime/base-tests.log",
".cursor-runtime/candidate-lint.log",
".cursor-runtime/candidate-tests.log",
".cursor-runtime/candidate-build.log",
@@ -3400,6 +3403,7 @@ jobs:
.cursor-runtime/implement-pr-body.out.md
.cursor-runtime/verify-result.json
.cursor-runtime/test-comparison.json
.cursor-runtime/base-tests.log
.cursor-runtime/candidate-lint.log
.cursor-runtime/candidate-tests.log
.cursor-runtime/candidate-build.log
@@ -3409,6 +3413,14 @@ jobs:
if: steps.patch.outputs.artifact_ready == 'true' && steps.verify.outputs.passed != 'true'
run: |
echo "Candidate patch was preserved, but candidate-specific verification failed." >&2
if [[ -f .cursor-runtime/test-comparison.json ]]; then
echo "Test comparison:" >&2
cat .cursor-runtime/test-comparison.json >&2 || true
fi
if [[ -f .cursor-runtime/verify-result.json ]]; then
echo "Verify result:" >&2
cat .cursor-runtime/verify-result.json >&2 || true
fi
exit 1
- name: Mark needs human on implement failure
@@ -4812,6 +4824,7 @@ jobs:
".cursor-runtime/fix.patch",
".cursor-runtime/fix-verify-result.json",
".cursor-runtime/fix-test-comparison.json",
".cursor-runtime/fix-base-tests.log",
".cursor-runtime/fix-candidate-lint.log",
".cursor-runtime/fix-candidate-tests.log",
".cursor-runtime/fix-candidate-build.log",
@@ -4831,6 +4844,7 @@ jobs:
.cursor-runtime/fix.patch
.cursor-runtime/fix-verify-result.json
.cursor-runtime/fix-test-comparison.json
.cursor-runtime/fix-base-tests.log
.cursor-runtime/fix-candidate-lint.log
.cursor-runtime/fix-candidate-tests.log
.cursor-runtime/fix-candidate-build.log
@@ -4840,6 +4854,14 @@ jobs:
if: steps.codex.outputs.action == 'fix' && steps.fixpatch.outputs.artifact_ready == 'true' && steps.fixverify.outputs.passed != 'true'
run: |
echo "Codex fix patch was preserved, but candidate-specific verification failed." >&2
if [[ -f .cursor-runtime/fix-test-comparison.json ]]; then
echo "Test comparison:" >&2
cat .cursor-runtime/fix-test-comparison.json >&2 || true
fi
if [[ -f .cursor-runtime/fix-verify-result.json ]]; then
echo "Verify result:" >&2
cat .cursor-runtime/fix-verify-result.json >&2 || true
fi
exit 1
- name: Mark needs human when no fix produced
+84 -36
View File
@@ -241,64 +241,111 @@ function compareTapResults(baseline, candidate) {
missingBaselineFailures.push(failure.name);
}
const result = ({
passed,
kind,
newFailures = [],
candidateFailures = candidate.exitCode === 0 ? [] : candidate.failures,
}) => ({
passed,
kind,
baselineFailures: baseline.failures,
candidateFailures,
newFailures,
missingBaselineSuccesses,
missingBaselineFailures,
});
if (candidate.exitCode === 0) {
const completeCleanRun =
// Require every baseline failure title to reappear as a same-named success
// before accepting a red-to-green transition. Aggregate counts alone would allow
// deleting the failing test and adding an unrelated passer.
const baselineFailCounts = countFailures(baseline.failures);
const baselineSuccessCountsForFix = countFailures(baseline.successes);
const candidateSuccessCountsForFix = countFailures(candidate.successes);
const unresolvedBaselineFailures = [];
for (const [name, failCount] of baselineFailCounts) {
const required =
failCount + (baselineSuccessCountsForFix.get(name) || 0);
const available = candidateSuccessCountsForFix.get(name) || 0;
for (let i = 0; i < required - available; i += 1) {
unresolvedBaselineFailures.push(name);
}
}
// A fully green candidate may rename success titles when quantitative
// coverage does not shrink, but only against a complete baseline summary.
// An incomplete baseline cannot prove coverage was preserved, so fail closed.
const quantitativeClean =
baseline.complete &&
candidate.complete &&
candidate.failCount === 0 &&
candidate.cancelledCount === 0 &&
unresolvedBaselineFailures.length === 0 &&
candidate.skippedCount <= baseline.skippedCount &&
candidate.todoCount <= baseline.todoCount &&
candidate.testCount >= baseline.testCount;
if (quantitativeClean) {
return result({ passed: true, kind: 'clean' });
}
const coverageShrunk =
baseline.complete &&
candidate.complete &&
candidate.failCount === 0 &&
candidate.cancelledCount === 0 &&
candidate.skippedCount <= baseline.skippedCount &&
candidate.todoCount <= baseline.todoCount &&
candidate.testCount >= baseline.testCount &&
missingBaselineSuccesses.length === 0 &&
missingBaselineFailures.length === 0;
return {
passed: completeCleanRun,
kind: completeCleanRun ? 'clean' : 'unclassified_failure',
baselineFailures: baseline.failures,
candidateFailures: [],
newFailures: [],
};
candidate.testCount < baseline.testCount &&
missingBaselineSuccesses.length > 0;
if (coverageShrunk) {
return result({
passed: false,
kind: 'missing_baseline_successes',
newFailures: missingBaselineSuccesses,
});
}
return result({
passed: false,
kind: 'unclassified_failure',
newFailures: unresolvedBaselineFailures.length
? unresolvedBaselineFailures
: missingBaselineSuccesses,
});
}
if (baseline.exitCode === 0) {
return {
return result({
passed: false,
kind: 'new_failures',
baselineFailures: [],
candidateFailures: candidate.failures,
newFailures: candidate.failures,
};
});
}
if (!baseline.complete || !candidate.complete) {
return {
return result({
passed: false,
kind: 'unclassified_failure',
baselineFailures: baseline.failures,
candidateFailures: candidate.failures,
newFailures: candidate.failures,
};
});
}
if (missingBaselineSuccesses.length) {
return {
return result({
passed: false,
kind: 'unclassified_failure',
baselineFailures: baseline.failures,
kind: 'missing_baseline_successes',
candidateFailures: candidate.failures,
newFailures: missingBaselineSuccesses,
};
});
}
if (candidate.exitCode !== baseline.exitCode) {
return {
return result({
passed: false,
kind: 'unclassified_failure',
baselineFailures: baseline.failures,
candidateFailures: candidate.failures,
newFailures: candidate.failures,
};
});
}
const baselineNonTapCounts = countFailures(baseline.nonTapOutput);
@@ -307,23 +354,21 @@ function compareTapResults(baseline, candidate) {
([line, count]) => count > (baselineNonTapCounts.get(line) || 0),
);
if (addedNonTapOutput) {
return {
return result({
passed: false,
kind: 'unclassified_failure',
baselineFailures: baseline.failures,
candidateFailures: candidate.failures,
newFailures: candidate.failures,
};
});
}
if (candidate.failCount === 0 || candidate.testCount < baseline.testCount) {
return {
return result({
passed: false,
kind: 'unclassified_failure',
baselineFailures: baseline.failures,
candidateFailures: candidate.failures,
newFailures: candidate.failures,
};
});
}
if (
@@ -331,13 +376,12 @@ function compareTapResults(baseline, candidate) {
candidate.skippedCount > baseline.skippedCount ||
candidate.todoCount > baseline.todoCount
) {
return {
return result({
passed: false,
kind: 'cancelled_tests',
baselineFailures: baseline.failures,
candidateFailures: candidate.failures,
newFailures: candidate.failures,
};
});
}
const baselineCounts = countFailures(
@@ -364,17 +408,16 @@ function compareTapResults(baseline, candidate) {
const reportedFailures = newFailures.length
? newFailures
: missingBaselineFailures;
return {
return result({
passed,
kind: passed
? 'baseline_only'
: newFailures.length
? 'new_failures'
: 'unclassified_failure',
baselineFailures: baseline.failures,
candidateFailures: candidate.failures,
newFailures: reportedFailures,
};
});
}
function parseArgs(argv) {
@@ -418,6 +461,11 @@ function main(argv = process.argv.slice(2)) {
if (result.newFailures.length) {
console.error(`New failures:\n- ${result.newFailures.join('\n- ')}`);
}
if (!result.passed && result.missingBaselineSuccesses?.length) {
console.error(
`Missing baseline successes:\n- ${result.missingBaselineSuccesses.join('\n- ')}`,
);
}
return result.passed ? 0 : 1;
}
+49 -9
View File
@@ -36,13 +36,14 @@ test('rejects a zero-exit candidate without a complete clean TAP summary', () =>
assert.equal(result.kind, 'unclassified_failure');
});
test('rejects replacing an existing successful test with an unrelated one', () => {
test('accepts renaming a successful test when quantitative coverage does not shrink', () => {
const result = compareTapResults(
parseTapResult(tap({ successes: ['kept test', 'removed test'] }), 0),
parseTapResult(tap({ successes: ['kept test', 'unrelated replacement'] }), 0),
);
assert.equal(result.passed, false);
assert.equal(result.kind, 'unclassified_failure');
assert.equal(result.passed, true);
assert.equal(result.kind, 'clean');
assert.deepEqual(result.missingBaselineSuccesses, ['removed test']);
const issueNumberName = compareTapResults(
parseTapResult(tap({
@@ -52,16 +53,55 @@ test('rejects replacing an existing successful test with an unrelated one', () =
successes: ['accepts packages that ship upstream #9999'],
}), 0),
);
assert.equal(issueNumberName.passed, false);
assert.equal(issueNumberName.passed, true);
assert.equal(issueNumberName.kind, 'clean');
const duplicateName = compareTapResults(
parseTapResult(tap({ failures: ['same name'], successes: ['same name'] }), 1),
parseTapResult(tap({ successes: ['same name', 'unrelated replacement'] }), 0),
// Clean candidate that fixes a prior red must keep the failing test title.
const fixedFailure = compareTapResults(
parseTapResult(tap({ failures: ['broken test'], successes: ['other test'] }), 1),
parseTapResult(tap({ successes: ['broken test', 'other test'] }), 0),
);
assert.equal(duplicateName.passed, false);
assert.equal(fixedFailure.passed, true);
assert.equal(fixedFailure.kind, 'clean');
// Deleting the failing test and adding an unrelated passer is not a clean fix.
const deletedFailure = compareTapResults(
parseTapResult(tap({ failures: ['broken test'], successes: ['other test'] }), 1),
parseTapResult(tap({ successes: ['other test', 'unrelated replacement'] }), 0),
);
assert.equal(deletedFailure.passed, false);
assert.equal(deletedFailure.kind, 'unclassified_failure');
assert.deepEqual(deletedFailure.missingBaselineFailures, ['broken test']);
});
test('rejects a clean candidate when the exact-base TAP summary is incomplete', () => {
test('accepts a green candidate that renames one success while adding another', () => {
const result = compareTapResults(
parseTapResult(tap({
successes: ['alpha', 'beta', 'gamma'],
tests: 8886,
}), 0),
parseTapResult(tap({
successes: ['alpha', 'beta', 'delta', 'epsilon'],
tests: 8887,
}), 0),
);
assert.equal(result.passed, true);
assert.equal(result.kind, 'clean');
assert.deepEqual(result.missingBaselineSuccesses, ['gamma']);
});
test('rejects deleting successful coverage even when the run stays green', () => {
const result = compareTapResults(
parseTapResult(tap({ successes: ['kept test', 'removed test'], tests: 12 }), 0),
parseTapResult(tap({ successes: ['kept test'], tests: 11 }), 0),
);
assert.equal(result.passed, false);
assert.equal(result.kind, 'missing_baseline_successes');
assert.deepEqual(result.missingBaselineSuccesses, ['removed test']);
assert.deepEqual(result.newFailures, ['removed test']);
});
test('rejects a clean candidate when the exact-base TAP summary is incomplete (fail closed)', () => {
const result = compareTapResults(
parseTapResult('base runner stopped early', 1),
parseTapResult(tap(), 0),
+3
View File
@@ -2293,6 +2293,9 @@ test('every code-writing Cursor path compares exact-base failures and preserves
assert.match(codex, /fix-test-comparison\.json/);
assert.match(codex, /steps\.fixpatch\.outputs\.artifact_ready == 'true'/);
assert.match(codex, /fix-candidate-tests\.log/);
assert.match(codex, /fix-base-tests\.log/);
assert.match(implement, /base-tests\.log/);
assert.match(followup, /followup-base-tests\.log/);
assert.ok(
codex.indexOf('name: Install test shell dependencies') <
codex.indexOf('name: Capture Codex-fix exact-base test baseline'),