Repository navigation
fix(compliance): make a failed consent record visible and retryable - #78
Merged
Merged
Conversation
Defects 7, 8 and 9 from the policy_selector rollout. 7 and 8 are one bug. record_signature's return value was discarded, so a failed write still painted the PR a clean success. The PR #67 short-circuit then skips any commit already carrying a successful status, so nothing ever came back: green PR, no consent record in signatures/<doc>.json, and no way to tell from the outside. 7 creates the gap; 8 makes it permanent. The signature comment is evidence of intent, but the JSON file is the record of record, and it was silently optional. Fix: capture the return value. On failure the status stays SUCCESS — the contributor did sign, and an infrastructure fault on our side is not theirs to pay for — but the description gains a "record pending" marker, and the short-circuit exempts marked statuses so the next sweep retries the write. A successful retry repaints the clean description, so the PR converges and stops being re-processed. get_existing_status_state is RENAMED to get_existing_status rather than having its return type changed in place. A caller left comparing the old name to "success" would silently evaluate False against a tuple, the #67 short-circuit would stop firing, and the repaint loop it exists to prevent would come back. An AttributeError is the better failure. 9: every PR is swept inside a try/except that logs and continues, so a sweep where every PR failed exited 0 and looked identical to a clean one. On a 5-minute cron with nobody reading logs that is invisible indefinitely. Failures are now collected and named, and the sweep exits non-zero, behind SWEEPER_STRICT_EXIT so a transient fault can be quieted without reverting code or disabling the schedule. An empty installation list is also a failure now rather than a quiet return. Note on scope: 9 catches EXCEPTIONS, not failed record writes — record_signature returns False rather than raising, so a pending record does not redden the sweep. The retry and the status description are what surface that. The two changes are complementary, not the same mechanism. Verification: * 126 tests, up from 101. cla_sweeper.py had no tests at all before this; it now has 11. * All 16 mutations caught by their named tests, tree restored clean after each: discarding the return value, painting clean regardless, blocking the contributor, dropping the exemption, pinning has_pending_record True/False, case-sensitive matching, drifting the marker away from the painted text, dropping failure counting, forcing exit 0, ignoring or inverting the kill switch, and making an empty repo list quiet again. * All 25 new tests provably killable — 21 by reverting a changed file, 4 characterisation tests by targeted mutation. * Existing test test_correct_sentence_passes_and_is_recorded was itself exercising a FAILED write: its fake never provided a writable signatures route, so record_signature returned False and the test asserted the clean description anyway. It passed because the failure was invisible even in the suite. Now given writable routes. * Suite runtime 9s -> 0.04s: the harness stubs the real time.sleep in the compliant path and record_signature's retry jitter. * Extracted ProcessSinglePrHarness so the new class does not inherit TestProcessSinglePr's 24 tests and re-run them under its own name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A line-level coverage pass over this branch's diff found one genuinely untested branch: the `...and N more` truncation in the sweep summary, which only fires above 20 failures. Everything else flagged was either a docstring continuation or an `else:` line the tracer does not emit an event for, with both arms covered. Both new tests mutation-proven: dropping the truncation, dropping the count line, and truncating short lists are each caught. Note the first version of the long-list test asserted 20 occurrences of the failure text and found 50. That was the test being wrong, not the code — each failure is logged inline as it happens AND up to 20 are repeated in the end-of-run digest. The cap is deliberately on the digest only; the full detail belongs in the log. The assertion now scopes itself to the summary block. 128 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defects 7, 8 and 9 from the policy_selector rollout. Defects 4–6 are deliberately not here — they all require a fetch failure, and the measured rate is zero across 214 evaluations in 7 sweeps, which is why #72 was closed.
7 and 8 are one bug
7 creates the gap, 8 makes it permanent. The signature comment is evidence of intent; the JSON file is the record of record — and it was silently optional.
Fix. Capture the return value. On failure the status stays SUCCESS — the contributor signed, and an infrastructure fault on our side isn't theirs to pay for — but the description gains a
record pendingmarker, and the short-circuit exempts marked statuses so the next sweep retries. A successful retry repaints the clean description, so the PR converges and stops being re-processed.get_existing_status_stateis renamed toget_existing_status, not just changed to return a tuple. A caller left comparing the old name to"success"would silently evaluateFalseagainst a tuple, the #67 short-circuit would stop firing, and the repaint loop it exists to prevent would come straight back. AnAttributeErroris the better failure mode.9 — a failed sweep must not look like a clean one
Every PR is swept inside a
try/exceptthat logs and continues, so a sweep where every PR failed exited 0 and was indistinguishable from success in the Actions UI. On a 5-minute cron with nobody reading logs, that's invisible indefinitely.Failures are now collected, named, and the sweep exits non-zero — behind
SWEEPER_STRICT_EXITso a transient fault can be quieted without reverting code or disabling the schedule. An empty installation list is now a failure too, rather than a quietreturn.Scope note, to avoid overclaiming: 9 catches exceptions, not failed record writes.
record_signaturereturnsFalserather than raising, so a pending record does not redden the sweep. The retry and the status description are what surface that. The two changes are complementary, not the same mechanism.An existing test was hiding the bug
test_correct_sentence_passes_and_is_recordednever gave its fake a writable signatures route, sorecord_signaturewas returningFalseon every run. The test asserted the clean description and passed — because the failure was invisible even inside the suite. It now uses writable routes, and the failure path has its own tests.Verification
cla_sweeper.pyhad no tests at all; it now has 11.test_failed_write_is_marked_in_the_descriptiontest_failed_write_still_passes_the_contributortest_success_with_pending_record_is_reprocessedhas_pending_recordpinned Falsetest_marker_is_detectedhas_pending_recordpinned Truetest_plain_success_is_still_skipped+ 1test_detection_is_case_insensitivetest_marker_is_detectedtest_failure_and_pending_states_are_unaffectedtest_any_failed_pr_returns_nonzerotest_kill_switch_suppresses_the_nonzero_exittest_default_is_stricttest_anything_else_leaves_strict_exit_ontest_no_repositories_is_a_failure_not_a_quiet_successtime.sleepin the compliant path andrecord_signature's retry jitter. Also extractedProcessSinglePrHarnessso the new class doesn't inherit and re-runTestProcessSinglePr's 24 tests under its own name.Behaviour worth a reviewer's judgement
A permanently failing write means that PR is re-processed every sweep indefinitely — repainting bumps
updated_at, keeping it in the lookback window. That's the #67 loop, deliberately re-opened for this narrow case, because the alternative is a consent record that never lands. It costs one status repaint per sweep for that PR, posts no comments, and is visible on the PR itself asrecord pending. I think that's the right trade, but it is a trade.🤖 Generated with Claude Code