Repository navigation
feat(verify): withhold SAFE when a diff weakens its own tests - #169
Conversation
A diff can pass by relaxing the tests that would catch it - removing an assertion, deleting a test, adding a skip. Verified against the post-diff suite that reads green, and on the trusted path SAFE (issue #163). fuseVerdict now withholds a would-be-SAFE to UNPROVEN when the diff weakened any changed test file, naming the weakened test. Detection is conservative (net-removed assertions, deleted/renamed tests, added skip/xfail); net-additive changes are strengthening and never flagged. Degrade-only, never UNSAFE - we cannot tell a self-weakening from a legitimate behavior change, only that SAFE is not earned. Additive testWeakening field on VerdictReport; testWeakening is a required fuseVerdict param (a forgotten one would silently skip the downgrade). The untrusted path is already floored by ADR-19; this closes the trusted path.
Red-first: a diff that changes behavior AND removes its covering assertion is SAFE on the pre-fix tree (trusted) and UNPROVEN after; a behavior-preserving change that strengthens its test still reaches SAFE. Unit coverage for the pure detector (removed assert, deleted test, added skip, unittest asserts, and the negatives: net-additive, neutral rewrite, newly-added file) and for the fuseVerdict downgrade both directions.
An `assert` line parked inside a new docstring was counted as an assertion, so removing the REAL assertion netted zero and the weakening went undetected - a ~2-line false negative on the exact #163 scenario (found in review). Strip triple-quoted strings, single-line strings, and comments before every count. Also records the SAFE-path invariant in verdict-fuse: testWeakening is checked in the trusted-SAFE branch (not the fall-through ladder), so any new SAFE-returning path must re-check it.
Docstring-mask now flagged (was red in review). One case per skip/xfail family and self.fail. A dedicated rename case. The integration test tightened from not-SAFE to UNPROVEN + reason + testWeakening + changedLinesCovered, so a coverage-gap UNPROVEN can't pass it green. Known-negative tests pin the documented heuristic limits (count-preserving compensation, semantic loosening) so the suite does not imply coverage it lacks.
Two blind spots in the line-count heuristic (CodeRabbit, PR #166), both count-preserving so the old regex netted zero and missed them: an `async def test_*` (pytest-asyncio/anyio) whose deletion or rename went unseen, and a compound `foo(); assert x` whose inline assertion was not counted. Match `async def`, and count a bare assert at a `;` boundary as well as line start. Both moves count MORE, the fail-safe direction. Pinned by an async rename (assert count unchanged) and an inline-assert removal, each red on the prior regex.
The base-tree read that feeds #163 swallowed every error as empty content, so a test file that EXISTS but fails to read (EACCES/EISDIR/EIO) was treated as newly added and its weakening went unflagged - a trusted would-be-SAFE could ride a gutted test (CodeRabbit, PR #166). And the read ran an attacker-shaped path through `readFile` before the shadow tree's containment check, a content-disclosure oracle. Both close here: `detectWeakenedTests` refuses a repo-escaping path with the same symlink-aware boundary the unified-diff intake uses (exported `resolvesInsideRepo`) and records it as weakening; a non-ENOENT read error is recorded as weakening; only ENOENT (a genuinely new test) is treated as strengthening. Deep checks are skipped once weakening floors the verdict. Fail-safe throughout: every uncertainty degrades, never grants SAFE.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (8)
Files not reviewed due to moderation or processing errors (4)
Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe verifier now compares changed test files with their pre-diff versions and detects selected weakening signals. Trusted would-be-SAFE results become UNPROVEN when findings exist. The report includes non-empty findings. Unit and integration tests cover detection, base-content reads, and verdict outcomes. ChangesTest weakening verification
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant verifyDiff
participant detectWeakenedTests
participant Repository
participant detectTestWeakening
participant fuseVerdict
verifyDiff->>detectWeakenedTests: repoRoot and edits
detectWeakenedTests->>Repository: read pre-diff test content
Repository-->>detectWeakenedTests: content or read error
detectWeakenedTests->>detectTestWeakening: old and new test content
detectTestWeakening-->>detectWeakenedTests: weakening findings
detectWeakenedTests-->>verifyDiff: findings
verifyDiff->>fuseVerdict: verification result and findings
fuseVerdict-->>verifyDiff: final verdict and report
Merge Risk: ⚪ Minimal · up to The reviewed change has no identified merge-blocking issue. It is ready for normal merge checks. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What
Withhold
SAFEwhen a diff weakens the very tests that would judge it. A trustedwould-be-
SAFEwhose covering evidence rests on tests the same diff relaxed(removed assertion, deleted/renamed test, added skip/xfail) now degrades to
UNPROVEN, naming the weakened file. Degrade-only: it can never grantSAFE.Closes #163.
How
src/verify/test-weakening.ts(new): pure detector over changed test files.Strips strings/comments, then flags a net drop in assertion count (pytest
assert,self.assertX,self.fail, inline; assert), a disappearing testname (
def/async def, delete or rename), and added skip/xfail markers.src/verify/verdict-fuse.ts:testWeakeningis a requiredfuseVerdictinputand an additive
VerdictReportfield; the downgrade lives in the trusted branch.src/verify/verify-diff.ts: reads each changed test file's pre-diff content andfails safe — a repo-escaping path is refused before the read, a non-ENOENT read
error is recorded as weakening, only ENOENT counts as a new (strengthening) test.
Evidence
--trusted): behavior change + gutted covering assertionreads
SAFEonmain,UNPROVENhere, withchangedLinesCovered === truepinned so a coverage-gap UNPROVEN can't pass it green. Strengthening stays
SAFE.each skip/xfail family, docstring-mask, inline assert, known-negatives) and the
intake fail-safe (read-error → weakening, escaping path → refused, ENOENT → new).
macOS/Ubuntu/Windows x Node 18/20/22 (660 tests). Same code here; CI re-runs.
Known limitations
Heuristic bar-raiser, not a proof, and degrade-only, so every gap leaves the
pre-change verdict, never a new false
SAFE. Count-preserving evasions (#164) andbroad-vs-covering scope (#165) are tracked as follow-ups. Python-shaped signals.
🤖 Generated with Claude Code
Summary by CodeRabbit