gitea-saas-core#29: transactional + retryable review outcome application #32

Merged
pbwraith merged 5 commits from issue-29 into main 2026-08-29 10:23:28 +02:00

5 Commits

Author SHA1 Message Date
Claude
b46c356d64 fix(#29): head-integrity gates — recovery requires exact merged head; reject missing reviewed head (bounce-fix)
Governor review 10539 findings, finding by finding (issue-29 branch, PR #32 retained):

1. Recovery must never attribute a DIFFERENT head's merge to a reviewed-head plan.
   recover_incomplete() adopted any live record with merged is True for the PR
   number without comparing heads: if plan A was persisted and then head B was
   merged externally, the retry row for A would debit/label/close/comment as
   though reviewed head A had been delivered. Crash-window adoption now
   requires BOTH positive merged proof AND live head_sha == the persisted
   planned/reviewed head_sha. A merged DIFFERENT head fails closed with a
   structured HEAD_MISMATCH result (persisted attempt + result entry), and
   performs NO debit, NO label removal, NO issue close, NO success comment;
   the row is left unadopted. The same-head crash-window success case is
   preserved.

2. A candidate with no reviewed head_sha must never reach the merge client.
   Passing expected_head_sha=None to merge_pr would arm the production
   client's backward-compatible live-head fallback and could merge a head the
   policy never reviewed. _apply_policy() now rejects a missing/empty
   head_sha BEFORE the plan write (review_state.head_sha is NOT NULL, so the
   DB would reject it anyway — now with the correct semantic failure) and
   _safe_merge() independently refuses to call the client without a pinned
   reviewed head. Zero merge attempts and zero downstream side effects.

New regressions (tests/test_review_policy.py):
- test_recovery_merged_different_head_fails_closed — plan at sha-A, live
  merged at sha-B: HEAD_MISMATCH, zero side effects, merged_step unmarked.
- test_missing_reviewed_head_never_reaches_merge_client — candidate with no
  head_sha: structured failure, no merge_pr call, no debit/label/close/comment.

Preserved: reviewed-SHA merge pin, production normalized merged/state shape,
closed-unmerged negative gate (state==closed never counts), durable recovery
independent of the open-PR scan, checkpoint/idempotency coverage, and the
same-head crash-window adoption.

Evidence: python3 -m pytest -q -> 203 passed, 1 skipped (was 201 passed, 1
skipped; +2 new tests). git diff --check clean. Files: gitea_saas/review_policy.py,
tests/test_review_policy.py.
2026-08-29 03:20:53 -05:00
Claude
30e71d44ba fix(#29): pin merge to reviewed head, positive merged-proof recovery, closed-unmerged guard (bounce-fix)
Governor review #2 findings on PR #32:

1. _safe_merge now passes the REVIEWED head sha to
   gitea.merge_pr(..., expected_head_sha=...) — the production client
   (issue #28) fails closed before any POST when the branch moved after
   review, so an unreviewed SHA can never be merged. Integration
   regression: SHA A reviewed / live SHA B -> no merge POST, no debit,
   no close; the moved head is re-reviewed and merged next poll.

2. Production client shape coordinated: GiteaClient._normalize_pr now
   carries the raw 'merged' boolean and 'state' in every normalized PR
   (list_open_prs and get_pr), so recover_incomplete can positively
   identify a merged PR. Tests exercise the production-shaped fake
   (merged/state fields, open-only list_open_prs, live-record mutation
   on merge) instead of a richer test-only dict. Crash-window adoption
   regression: merged_pr checkpoint missed, live record reports
   merged=True -> recovery finishes debit/label/close/comment without a
   second merge request.

3. state == 'closed' is no longer treated as merged anywhere: _safe_merge
   and recover_incomplete adopt a merge only on 'merged is True'.
   Negative regression: a closed-but-unmerged PR with a pending outcome
   plan produces no debit, no label removal, no issue close, no success
   comment.

Evidence: 201 passed, 1 skipped (full suite); git diff --check clean.
2026-08-29 02:59:52 -05:00
Claude
d522ea23e4 Merge remote-tracking branch 'origin/main' into issue-29 2026-08-29 02:55:48 -05:00
pbwraith-worker
c3915d2533 fix(review_policy): recover stranded post-merge outcomes outside the open-PR scan (#29) (bounce-fix) 2026-08-28 14:28:37 -05:00
Claude
6cff1599d2 gitea-saas-core#29: transactional + retryable review outcome application
A head SHA is now recorded as completed (reviews table) only after its
required policy action fully succeeded; any failure — verdict generation
or a label/comment/merge/close/debit boundary — is persisted in the new
review_state table (attempts, last_error, merged_step/closed_step
checkpoints) and reported as a structured outcome=failed result, leaving
the same unchanged head eligible for retry on the next poll.

Retries resume from checkpoints so a partial side effect can never
double-merge a PR or double-close an issue; the delivery debit keeps its
billing-core idempotency key (<repo>#<pr>@<head_sha>). Legacy reviews
rows are preserved untouched (completed history); no migration needed.

Closes #29
2026-08-28 13:43:29 -05:00