Skip to content

refactor(gates): migrate retry-client carriers onto _gitapex_github_http.py - #1472

Merged
tvna merged 28 commits into
mainfrom
claude/gitapex-pr-729-1yu06c
Aug 30, 2026
Merged

refactor(gates): migrate retry-client carriers onto _gitapex_github_http.py#1472
tvna merged 28 commits into
mainfrom
claude/gitapex-pr-729-1yu06c

Conversation

@tvna

@tvna tvna commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

Migrates 5 hand-copied GitHub API retry-client implementations onto the
shared .github/scripts/_gitapex_github_http.py module, closing criterion
1 of issue #729 (up to 3 separate PRs total per that issue's own
Constraints; this is PR 1 of 3).

Facts

Assumptions

  • This PR is scoped to criterion 1 only (GitHub API retry client).
    Criteria 2 (stdin/file decode) and 3 (JSON shape guard) are separate,
    later PRs per the issue's own Constraints -- not addressed here.

Risk / blast radius

Touches 5 CI gate/report scripts' own GitHub-API call paths (retry/error
handling only -- no endpoint URLs, payloads, or business logic changed).
Public function signatures and behavior are preserved as a pure extraction
refactor, with the following disclosed, individually-verified exceptions
found by this PR's own mandatory Step 8 refactor + independent adversarial
review passes (each verified live with a scripted repro, not by reading):

  1. gitapex_stale_retro_stub_autoclose.py: an HTTP 200 response with an
    unparseable or empty body now raises GitHubApiError instead of
    silently returning an empty list
    , since _fetch_issues_page was
    replaced by the shared fetch_json_page. Verified unreachable in
    documented GitHub behavior (the List repository issues and List
    issue comments
    REST endpoints document only 200/301/404/422 and
    200/404/410 respectively, with an empty result set always [], never a
    zero-byte body) -- reachable only via a flaky-proxy/CDN truncation, the
    same case _gitapex_github_http.py's own docstring already documents
    as an intentional raise. In that one reachable case the OLD behavior
    was actively worse: a truncated comments-list response made
    _has_close_comment return False, so close_stub_issue would post a
    duplicate close comment -- defeating that function's own documented
    duplicate-comment guard. The new loud-failure behavior is a net
    improvement, not a regression; no fix applied.
  2. gitapex_sync_pr_publish.py: outgoing JSON request-body whitespace
    changed
    from compact (separators=(",", ":")) to default spacing for
    apply_call's REST bodies (graphql_call keeps compact separators,
    moved verbatim). Confirmed harmless: JSON insignificant whitespace is
    non-semantic (RFC 8259 section 2.7), Content-Length is recomputed from the
    encoded bytes, and this file's only REST bodies are plain field maps
    (file content goes through GraphQL, not a byte-exact REST body). Not
    normalized -- the new spacing matches the other 4 migrated carriers.
  3. gitapex_sync_pr_publish.py: apply_call's caught-exception surface
    widened
    from HTTPError/URLError to the shared
    (OSError, http.client.IncompleteRead), matching the other 4 carriers'
    already-established shape. Confirmed a net improvement: a body-read
    IncompleteRead/TimeoutError/ConnectionResetError previously
    escaped as an uncaught exception and now retries; apply_call has no
    external callers besides this file's own internals, and its 2 REST
    POSTs are name-keyed creates GitHub itself dedupes (documented 201/409
    or 201/403 outcomes, no observed second-success path), so the wider
    retry surface introduces no duplicate-creation hazard.
  4. Two cosmetic-only, harmless log-text changes, both confirmed to have
    zero consumers via a repo-wide grep: gitapex_gate_retro_title_convention_citation.py's
    is_resolvable_issue now emits one extra stderr diagnostic line on a
    404 (return value/attempt-count/backoff unchanged); gitapex_sync_pr_publish.py's
    network-error log text changed from HTTP 000 to HTTP network error
    (the shared module's format_code convention, now applied
    consistently across all 5 carriers instead of only 4).

Every carrier's own existing unit test suite plus new tests on the shared
module (including a mutation-tested regression test added during the
adversarial-review pass, confirmed to kill a mutant that the pre-fix suite
missed) are the primary defense against further behavioral drift.

Rollback

Revert this PR's commits. No schema, data, or external-state change --
a plain code revert restores the prior per-file copies exactly.

Verification

Acceptance Criteria Map (this PR's scope):

Criterion Interpretation Planned ops Proof method Residual risk Result
5 carriers' hand-copied retry implementations replaced by delegation to _gitapex_github_http.py Add request_with_retry/call_json/graphql_call to the shared module; each carrier delegates its retry-loop body while keeping its own public signature/behavior Add 3 functions + tests to _gitapex_github_http.py; migrate 5 carrier files New + existing unit tests pass; repo-wide grep for the old retry-loop pattern returns zero matches in the 5 migrated carriers sync_pr_publish.py's apply_call is dependency-injected deep into its own internal functions -- verified byte-identical wiring by the Step 8 adversarial review's own inspect.signature identity check, not assumed PASS -- see command output below; grep -rn "def _call(" .github/scripts/<5 carriers> returns zero matches
hooks/gitapex_check_pr_issue_acm_disclosure.py stays excluded; gets a parity/sync test instead Mirror tests/test_gitapex_check_skill_audit_disclosure_hook_sync.py's pattern Add one new test file; hooks file itself untouched New parity test passes; hooks file has zero diff None identified PASS -- 8/8 parity tests pass; git diff on the hooks file is empty
Known unguarded-json.loads-on-2xx bug is NOT fixed here (criterion 3's own separate PR) Preserve existing behavior byte-for-byte in call_json No guard added Disclosed explicitly in this PR's Non-goals A reviewer could mistake this for an oversight if not disclosed clearly PASS, with one nuance disclosed in Non-goals below (the shared fetch_json_page path used by gitapex_stale_retro_stub_autoclose.py was already raise-on-unparseable before this PR and stays that way -- see Risk item 1 above)

Commands run (final head, after the mandatory Step 8 refactor + adversarial
review passes):

$ uv run --frozen pytest -q   # full suite, matching CI's own pytest job scope
6841 passed in 123.99s (0:02:03)

$ uv run --frozen pytest tests/test_gitapex_github_http.py \
    tests/test_gitapex_gate_acm_issue_disclosure.py tests/test_gitapex_post_merge_retro.py \
    tests/test_gitapex_stale_retro_stub_autoclose.py \
    tests/test_gitapex_gate_retro_title_convention_citation.py \
    tests/test_gitapex_sync_pr_publish.py \
    tests/test_gitapex_check_pr_issue_acm_disclosure_github_http_sync.py hooks -q --no-cov
884 passed

$ uv run --frozen mypy --config-file pyproject.toml tests hooks .github/scripts evals/scripts \
    skills/battle-testing-a-skill/scripts skills/scorer-gated-skill-edits/scripts \
    skills/evaluating-skill-quality/scripts skills/auditing-agent-product-scope/scripts \
    skills/evaluating-deterministic-gate-quality/scripts skills/drafting-issues/scripts \
    skills/executing-a-branch-plan/scripts
Success: no issues found in 283 source files
(plus the other 5 independent skill-script directories test.yml checks separately -- all clean)

$ uv run --locked ruff check --output-format github .
(no output -- clean)

$ uv run --frozen xenon --max-absolute E --max-modules B --max-average A \
    --exclude "apm_modules/*,skills/evaluating-skill-quality/scripts/gitapex_check_skill_shape.py" .
(exit 0 -- clean)

$ grep -rn "def _call(" .github/scripts/gitapex_gate_acm_issue_disclosure.py \
    .github/scripts/gitapex_post_merge_retro.py .github/scripts/gitapex_stale_retro_stub_autoclose.py \
    .github/scripts/gitapex_gate_retro_title_convention_citation.py .github/scripts/gitapex_sync_pr_publish.py
(no output -- the old hand-copied retry-loop pattern is gone from all 5 carriers)

Non-goals

  • Criterion 2 (stdin/file UTF-8 decode helper) and criterion 3 (JSON
    shape-guard helper) are separate, later PRs per issue refactor(gates): extract shared stdin-decode / GitHub-API-retry / JSON-shape modules out of .github/scripts/*.py #729's own
    Constraints section (up to 3 PRs total) -- not in this PR's scope.
  • The known "unguarded json.loads on a 2xx response body" bug in
    call_json (used by 3 of the 5 carriers plus hooks/gitapex_check_pr_issue_acm_disclosure.py,
    which stays out of scope) is deliberately NOT fixed in this PR -- that
    is explicitly criterion 3's own scope. Nuance found during review:
    gitapex_stale_retro_stub_autoclose.py's list-fetch call sites go
    through the shared fetch_json_page (not call_json), which already
    raised on an unparseable 2xx body before this PR too -- so criterion 3's
    remaining scope for that one file is narrower than "6 unguarded sites"
    originally estimated; see Risk item 1 above for the detail.
  • Two additional pre-existing gaps were found during review, confirmed
    NOT introduced by this PR (reproduced identically against the
    pre-migration base commit), and left out of this PR's own scope as
    genuinely separate concerns:
    • gitapex_stale_retro_stub_autoclose.py's list_open_retro_issues/_has_close_comment
      don't shape-check fetch_json_page's return value against its own
      documented "every caller must shape-check it before use" contract,
      unlike the sibling gitapex_gate_acm_issue_disclosure.py, which
      already does this correctly. Natural fit for criterion 3's own PR.
    • gitapex_sync_pr_publish.py's _create_branch_ref/_create_pr are
      non-idempotent creates that retry 3x instead of the
      max_attempts=1 pattern this repository's own RFC 9110 convention
      already establishes elsewhere in this same file family -- likely
      low-severity in practice (both endpoints appear name-keyed/GitHub-deduped)
      but inconsistent once fixed everywhere else.
    • (Queuing these as follow-up task suggestions was attempted but the
      tooling timed out; flagging here instead so they aren't lost.)
  • Two more pre-existing gaps were found by drafting-a-pr-to-merge's own
    Step 8 independent review (reuse-and-simplification axis), confirmed
    NOT introduced by this PR, and left out of this PR's own scope the same
    way as the two items above:
    • _gitapex_github_http.py's own graphql_call still duplicates
      request_with_retry's request/retry-loop shape internally instead of
      delegating to it (found by this PR's own Step 8 independent review,
      reuse-and-simplification axis) -- confirmed real, but not fully
      fixable without giving request_with_retry a pluggable
      is-this-retryable hook, since GraphQL retries on a 200 response
      carrying an error-marker body while request_with_retry unconditionally
      breaks on any 2xx. Left as a known design gap rather than widening
      this PR into that refactor; a natural fit for a follow-up PR.
    • graphql_call has no max_attempts parameter, unlike its two
      siblings (request_with_retry, call_json), despite gitapex_sync_pr_publish.py
      using it for a non-idempotent createCommitOnBranch write (found by
      the same review). Confirmed pre-existing (the pre-migration local
      copy already hardcoded the same 3-attempt loop) and low severity in
      practice (the mutation's own expectedHeadOid precondition makes a
      retried-after-lost-success attempt fail loudly rather than duplicate
      the commit) -- left unfixed here for the same narrow-scope reason as
      the item above.

Checklist

  • Tests pass locally
  • Docs updated if behavior changed -- N/A, no user-facing docs beyond the code's own docstrings, which are updated
  • Issue number cited in every commit
  • N/A -- no skills/*/SKILL.md, design doc, security-relevant skill, or new deterministic checker script added by this PR (existing .github/scripts/*.py files are modified, not newly added)
  • N/A -- no evals/*/split.md Kept-edit-log entry in this PR
  • N/A -- no new Stop-boundary bullet or named dispatch branch added to any skill in this PR

Skill audit evidence

This PR modifies deterministic checker/gate scripts under .github/scripts/*.py
(2 of the 5 migrated carriers, gitapex_gate_acm_issue_disclosure.py and
gitapex_gate_retro_title_convention_citation.py, are themselves
gate_*-named/.gitapex/ssot.json-registered gates; gitapex_stale_retro_stub_autoclose.py
is also ssot-registered), triggering skill-audit-disclosure's
process-disclosure checks:

  • checker-script-adversarial-review: RAN -- executing-a-branch-plan's
    Step 8 sub-step 2 (an independent, fresh-subagent adversarial code
    review over the full accumulated diff) reviewed all 5 carriers plus the
    shared module for correctness, with live/scripted repros rather than
    inspection alone -- see Risk section above for its findings.
  • deterministic-gate-quality: RAN -- the same Step 8 review explicitly
    checked argument-order/value preservation at every migrated call site
    (a 50-scenario differential harness comparing old vs. new behavior),
    the non-idempotent-POST max_attempts=1 safety pattern at all 4
    applicable call sites, and message-format byte-identity on raised
    errors -- the class of defect evaluating-deterministic-gate-quality's
    dimension 15 (fail-closed default) and this repository's own PR fix(plugin): brace ${CLAUDE_PLUGIN_ROOT} so apm-deployed hooks resolve #651
    precedent (three fail-open defects that passed happy-path tests alone)
    exist to catch.
  • defeat-test-disclosure: RAN -- Step 8 sub-step 1 (refactor/simplify)
    found and fixed a genuine detection-logic gap in the shared module's own
    test suite: request_with_retry's except (OSError, http.client.IncompleteRead)
    branch had no direct test of its own (only indirectly covered through 3
    carrier test files). A mutation test confirmed the gap (collapsing the
    tuple to a bare except OSError left the shared module's own 29-test
    file fully green) and a new regression test was added and confirmed to
    kill that mutant (1 failed, 31 passed with the mutant present, 32 passed without) -- committed to the test suite per this gate's own
    "commit the case ... one only constructed and run once ... can be
    silently reintroduced" requirement. No detection logic was narrowed by
    this PR (a pure extraction refactor), so no additional narrowing-edit
    defeat case was required.

Merge gate: independent review

This PR is also subject to the independent-review-pending required
status check (see .github/workflows/independent-review-pending.yml /
.github/scripts/gitapex_gate_independent_review_pending.py). It stays
pending/failing until a ## Independent review verdict section naming
this PR's current head commit is recorded in this body --
drafting-a-pr-to-merge's own Step 8 records it once its independent
review completes. There is nothing for you to do here now: do not
pre-fill this section yourself, and do not remove this note.

Execution log

  • PlanApproved -- Branch Plan/ACM committed as
    docs/superpowers/plans/2026-08-29-claude-gitapex-pr-729-1yu06c.md;
    1 wave-1 task (shared module) + 6 wave-2 tasks (5 carrier migrations +
    1 hooks parity test) decomposed, no file-ownership conflicts.
  • BranchPublished -- claude/gitapex-pr-729-1yu06c pushed with the
    task-list commit.
  • TaskCompleted(T1) -- request_with_retry/call_json/graphql_call
    added to _gitapex_github_http.py (commit a77a56a5); tests/mypy/ruff
    clean. Governance-path pre-filter: no-match; full diff review: clean.
  • BranchDriftMerged (x3) -- shared branch merged origin/main after
    wave 1, after wave 2, and after the Step 8 refactor pass (12, 31, and 8
    commits behind respectively, all clean merges, tests re-confirmed green
    each time).
  • CoverageGapClosed -- Codecov flagged 7 patch-coverage-missing lines in
    wave 1's own new functions; closed with 15 new tests before wave 2
    dispatched (18 -> 29 tests in tests/test_gitapex_github_http.py).
  • TaskCompleted(T2-T7) -- all 6 wave-2 tasks completed
    (gitapex_gate_acm_issue_disclosure.py 3e37f673,
    gitapex_post_merge_retro.py 7bd7a1e4,
    gitapex_stale_retro_stub_autoclose.py b020aad9,
    gitapex_gate_retro_title_convention_citation.py 62a98992,
    gitapex_sync_pr_publish.py 9dc6fa78, hooks parity test 1e725250);
    merged onto the shared branch with no conflicts, diffs reviewed
    (governance-path pre-filter + full model review, clean).
  • GateFixed -- stdlib-only-claim-drift correctly flagged a stale bare
    python3 usage example in gitapex_sync_pr_publish.py's docstring
    (pre-existing staleness surfaced by this PR's new import); fixed to
    uv run --frozen python3 ... matching the established convention.
  • Step8RefactorPass -- fresh-subagent refactor/simplify pass over the
    full accumulated diff: 8 findings fixed (dead code, doc drift, header
    duplication in graphql_call, a private cross-module import promoted
    to public, a test that had stopped catching drift, a test name
    overstating its own coverage), 3 genuine behavioral differences
    surfaced and handed to the adversarial-review pass rather than fixed
    outside that pass's own scope (commits 39a1f3a3, 58caf28a,
    e33a5ac0, 4d0c47a3).
  • Step8AdversarialReview -- independent fresh-subagent adversarial
    review verified all 3 handed-off findings live (see Risk section
    above), found and fixed one test-coverage gap via mutation testing
    (commit 94c9f3ae), and confirmed clean on a full independent
    correctness pass (call-site argument audit, non-idempotent-POST audit,
    dependency-injection audit, import-graph audit) plus two pre-existing,
    out-of-scope findings noted in Non-goals.
  • ReadyForReview -- all tasks complete, Step 8 clean, remote state
    confirmed to match local; branch-plan-executing label removed, PR
    marked ready for review. Ownership passes to drafting-a-pr-to-merge.
  • Step8IndependentReviewFixes -- a second round of Step 8 fixes
    (commit 1424f889) applying findings from drafting-a-pr-to-merge's
    own inner-layer review (reviewing-an-artifact, 5 fresh isolated
    subagents against the PR-narrative-redacted diff): fixed a stale
    workflow-comment reference to the deleted _call() retry loop
    (.github/workflows/post-merge-retro.yml), 2 contradictory/false
    docstring claims (gitapex_sync_pr_publish.py's module docstring and
    format_code's own docstring), and trimmed
    tests/test_gitapex_post_merge_retro.py/tests/test_gitapex_stale_retro_stub_autoclose.py's
    own leftover redundant retry-mechanic tests to match this PR's own
    established precedent. 2 additional pre-existing, out-of-scope
    findings recorded in Non-goals above. Full suite (6923 passed),
    mypy, and ruff re-confirmed clean.

Independent review verdict

  • Verdict: CLEAN
  • Verified commit: 1424f88

Outer layer: this repository does not have Anthropic's Claude Code
Review GitHub App configured (absent from this PR's own check-run list).
GitHub Copilot's copilot-pull-request-reviewer[bot] was requested via
request_copilot_review and, as of this recording, has not posted a
review after roughly 20 minutes -- longer than its typical turnaround,
and consistent with this repository's other third-party review bots
(Devin, CodeRabbit) both currently running in a degraded/non-functional
mode on this repo (trial/credit and star-count gating, per their own
posted comments). Disclosed per this gate's own weaker-signal rule:
treat this layer as did-not-run rather than a pass.

Inner layer: reviewing-an-artifact (low effort, 5-axis fan-out --
correctness, blast-radius, reuse-and-simplification, convention,
security -- each a fresh, isolated subagent against this PR's own diff
with PR/commit narrative redacted) ran twice this session: once during
executing-a-branch-plan's own Step 8 (see Execution log above), and
again here as drafting-a-pr-to-merge's own mandatory inner layer.
4 confirmed findings from this second pass were fixed and pushed
(commit 1424f889; see Execution log entry Step8IndependentReviewFixes
above for the full list). 2 additional confirmed-but-pre-existing
findings and 1 minor cosmetic nit were disclosed rather than fixed,
staying within this PR's own narrow scope (see Non-goals above for the
2 pre-existing findings; the cosmetic nit -- a test helper defined after
its first inline-equivalent use in tests/test_gitapex_github_http.py
-- has zero functional impact and was judged not worth its own Non-goals
entry). Zero security-tier findings, confirmed or unconfirmed-concern,
on any pass.

Related Issue

Refs #729

@tvna
tvna deployed to ruleset-verify August 29, 2026 20:51 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6dc01d1b-a5c4-4148-acce-ccc493d45d35


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.62%. Comparing base (c967270) to head (1424f88).
⚠️ Report is 29 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1472      +/-   ##
==========================================
+ Coverage   99.59%   99.62%   +0.03%     
==========================================
  Files         133      133              
  Lines       23507    23315     -192     
  Branches     2840     2816      -24     
==========================================
- Hits        23411    23227     -184     
+ Misses         96       88       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tvna
tvna deployed to ruleset-verify August 29, 2026 23:04 — with GitHub Actions Active

tvna commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

Status on the two failing required checks, both expected mid-execution states, not regressions:

  • independent-review-pending: stays failing by design until drafting-a-pr-to-merge's own Step 8 records an ## Independent review verdict section, per this PR's own "Merge gate: independent review" note.
  • skill-audit-disclosure: this PR touches .github/scripts/*.py files, which triggers checker-script-adversarial-review and defeat-test-disclosure disclosure requirements (and deterministic-gate-quality, RAN-only, once the gate-named/.gitapex/ssot.json-registered carriers land in wave 2). This PR is still mid-execution (executing-a-branch-plan's wave 2 is dispatching now); a ## Skill audit evidence section with honest disclosure lines will be added once the mandatory Step 8 refactor/adversarial-review pass actually runs against the accumulated diff, rather than pre-filled now with a disclosure that hasn't happened yet.

Both will resolve together once execution reaches Step 8/9. No fix needed for either right now.


Generated by Claude Code

@tvna
tvna deployed to ruleset-verify August 29, 2026 23:14 — with GitHub Actions Active
@tvna
tvna deployed to ruleset-verify August 29, 2026 23:34 — with GitHub Actions Active
claude added 2 commits August 29, 2026 23:40
…empts rationale

graphql_call arrived from gitapex_sync_pr_publish.py with its own four
hand-written add_header calls, re-declaring inside the shared module the
exact header set build_headers (one function above it) already builds for
the REST path. Route it through build_headers instead.

call_json's docstring gains the max_attempts/idempotency rationale that
was deleted along with the per-carrier _call docstrings it used to live
in -- gitapex_post_merge_retro.py and gitapex_stale_retro_stub_autoclose.py
both cite "call_json's own docstring" at their max_attempts=1 call sites,
a reference that resolved to nothing until now.

Refs #729
_format_code and _HTTP_TIMEOUT_SECONDS both belonged to the inline retry
loop and _default_opener this script deleted when it moved onto
_gitapex_github_http.request_with_retry. Neither has a production caller
left; _format_code survived only because a unit test asserted on it
directly, and the timeout constant only because a test compared the real
(shared-module) timeout against this module's now-unrelated copy of the
same number. Delete both, delete the private-helper unit test with no
remaining subject, and repoint the timeout assertion at the constant that
actually drives it.

The module docstring gains the _gitapex_github_http delegation paragraph
the other four migrated carriers already carry, and apply_call's own
docstring now records the disclosed network-failure message-text
difference that until now was documented only in a test comment.

Refs #729
claude added 3 commits August 29, 2026 23:42
The migration gave the shared module its first cross-module consumer of
this helper: gitapex_gate_retro_title_convention_citation.py raises the
GitHubApiError itself (request_with_retry hands it a raw status code and
never sees it again), so it has to render the code the same way the
shared module does. It did that by importing _format_code -- the only
place in .github/scripts/ importing an underscore-prefixed name from
another module; every other cross-module import there names public
symbols only. Promote the helper rather than let the exception stand, or
re-inline the two-line convention this PR exists to stop copying.

No behavior change: the function body, every message it feeds, and every
caller's output are identical.

Refs #729
… it checks

The test asserted only the signature default for `opener` while its name
claimed it also covered the `sleeper=None -> time.sleep` default, which
nothing in the file exercises. A name that overstates coverage is how a
gap stays invisible; state the real boundary in the name and the comment.

Refs #729
@tvna
tvna deployed to ruleset-verify August 29, 2026 23:47 — with GitHub Actions Active
tests/test_gitapex_gate_acm_issue_disclosure.py's migration comment says
the retry mechanics it stopped testing locally -- "5xx retry,
IncompleteRead retry, repeated-network-failure raise" -- are "covered
there by tests/test_gitapex_github_http.py". Two of those three were not.
Only the 5xx path had a direct test in the shared module's own test file;
request_with_retry's `except (OSError, http.client.IncompleteRead)` arm
and call_json's raise-after-exhausted-network-retries path had none.

Established by mutation, not by reading: collapsing that except clause to
a bare `except OSError` -- which reintroduces the crash-instead-of-retry
bug, since http.client.IncompleteRead is an HTTPException and not an
OSError subclass -- left all 29 tests in tests/test_gitapex_github_http.py
green. Three carrier test files did catch it transitively, so the branch
was never uncovered repo-wide; but the shared primitive's most subtle line
had no test of its own, and the comment pointing readers at this file for
it was wrong.

Three tests added, no production change:
- request_with_retry retries an IncompleteRead body read (this kills the
  mutant above);
- it treats a URLError as the sentinel code 0 carrying `str(error)` as the
  body, retrying like a 5xx instead of early-breaking;
- call_json still surfaces GitHubApiError, rendered through format_code as
  "HTTP network error", once those attempts are exhausted -- the direct
  replacement for the test_call_raises_after_repeated_network_failure case
  deleted from the acm gate's own test file during the migration.

Refs #729
@tvna
tvna deployed to ruleset-verify August 30, 2026 00:02 — with GitHub Actions Active
@tvna tvna removed the branch-plan-executing label Aug 30, 2026 — with Claude
@tvna
tvna marked this pull request as ready for review August 30, 2026 00:14
…dant tests

Refs #729

Step 8 adversarial review (reviewing-an-artifact, reuse-and-simplification
axis) found graphql_call's exception handling narrower than
request_with_retry's own: `except urllib.error.URLError` alone misses
http.client.IncompleteRead (not a URLError/OSError subclass), letting a
stalled/truncated GraphQL body read escape uncaught instead of retrying.
Widened to `except (OSError, http.client.IncompleteRead)` to match, with
a regression test, and corrected the docstring's "moved verbatim ...
already correct" claim now that it names two real edits.

Also closes out the two remaining Step 8 findings: a direct format_code
unit test (previously only exercised indirectly through error messages),
and trimming test_gitapex_sync_pr_publish.py's graphql_call block down to
an identity-wiring assertion -- that module re-exports the shared
module's graphql_call unwrapped, so the retry/backoff/transient-marker
behavior it re-tested was already covered, more precisely, in
tests/test_gitapex_github_http.py.
@tvna
tvna deployed to ruleset-verify August 30, 2026 00:35 — with GitHub Actions Active
Refs #729

drafting-a-pr-to-merge's Step 8 inner layer (reviewing-an-artifact,
5 fresh isolated subagents against the redacted diff) surfaced 4
genuine, independently-verified fixes beyond what the earlier
refactor/adversarial pass already caught:

- blast-radius axis: .github/workflows/post-merge-retro.yml's own header
  comment still cited the deleted local `_call()` retry ceiling; updated
  to name the actual `_gitapex_github_http.call_json` calls it now
  describes.
- convention axis: gitapex_sync_pr_publish.py's module docstring claimed
  graphql_call was "moved wholesale ... was the correct one", directly
  contradicting the function's own docstring (which discloses the
  IncompleteRead exception-handling fix made on arrival). Corrected, and
  apply_call's own docstring now enumerates all three disclosed
  behavioral differences from its former local loop (body-text wrapping,
  JSON separator spacing, network-error display text) instead of only
  the first.
- convention axis: format_code's docstring claimed "every other
  cross-module import ... names public symbols only", falsified by
  gitapex_apply_rulesets.py's pre-existing private `_HTTP_TIMEOUT_SECONDS`
  import. Corrected to name that exception instead of denying it exists.
- convention axis: tests/test_gitapex_post_merge_retro.py and
  tests/test_gitapex_stale_retro_stub_autoclose.py still carried the full
  set of retry-mechanic tests (5xx retry, persistent-4xx raise,
  network-failure raise, IncompleteRead retry) that duplicate coverage
  now centralized in tests/test_gitapex_github_http.py -- the same
  inconsistency already fixed for test_gitapex_gate_acm_issue_disclosure.py
  and test_gitapex_sync_pr_publish.py earlier in this PR. Trimmed both to
  one integration-style test each confirming GitHubApiError still
  surfaces correctly, matching the established precedent; kept the
  close_stub_issue PATCH-close wiring test that proves this call site
  does NOT inherit the comment-POST's max_attempts=1, dropping only its
  redundant IncompleteRead variant.

Security, correctness, and reuse-and-simplification axes found no new
defects requiring a fix: their candidate findings were independently
verified to already be disclosed in this PR's own Risk/Non-goals
sections (the 404 stderr log line, JSON separator change, and
network-error display text change), or are pre-existing gaps confirmed
unchanged since before this migration (graphql_call's own duplicated
retry-loop shape vs. request_with_retry, and its lack of a max_attempts
parameter).

Full suite: 6923 passed. mypy/ruff clean.
@tvna
tvna deployed to ruleset-verify August 30, 2026 00:49 — with GitHub Actions Active
@tvna
tvna marked this pull request as draft August 30, 2026 00:59
@tvna
tvna marked this pull request as ready for review August 30, 2026 04:22
@tvna
tvna merged commit ae8c090 into main Aug 30, 2026
37 of 38 checks passed
@tvna
tvna deleted the claude/gitapex-pr-729-1yu06c branch August 30, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants