Resolves #15454
Summary
Surfaced during the #15359 / PR #15419 review — @neo-gpt's RC established that DevIndex.services.GitHub.query() must not replay a non-idempotent GraphQL mutation after an ambiguous outcome. PR #15419 (merged) closed the two shared-classifier paths (transport catch + 200-body). This closes the two PRE-EXISTING sibling paths #15419's scope deliberately left: the 5xx/403 retry and the in-body 502/504 gateway retry, which still replayed mutations.
The fix — extend the retry-authorization gate to all ambiguous paths
Retry classification (is it transient?) and retry authorization (is replay safe?) are separate decisions. A 5xx server error or an in-body 502/504 gateway error leaves a mutation's server-side outcome unknowable — the write (OptIn addComment / issue-close) may have applied before the failure — so replaying it can duplicate.
- 5xx (
>= 500) now fails loud for a mutation via the same #isMutation gate #15419 introduced; a 403 rate-limit is a pre-execution rejection (safe to replay) and stays retryable.
- The in-body 502/504 gateway path now fails loud for a mutation likewise.
- Idempotent reads (
query) retry all four paths unchanged.
GitHub.query() is now uniformly mutation-safe across all four ambiguous retry paths: transport disconnect + 200-body (#15419) and 5xx + in-body-gateway (this PR).
Deltas from ticket
None — the prescribed shape (extend the #isMutation authorization gate to the 5xx/gateway paths). The #isMutation helper + the transport/200-body gates are already on dev via the merged #15419.
Evidence: L2 (unit witnesses over both transports' real terminals) → L2 required (a retry-authorization contract; no runtime surface). Residual: none.
Test Evidence
npm run test-unit -- test/playwright/unit/app/devindex/GitHubService.spec.mjs → 22 passed (2 new discriminating witnesses + the 20 from #15419).
- New witnesses:
query does NOT replay a mutation after a 5xx server error — a read from the SAME 5xx retries + query does NOT replay a mutation after an in-body gateway (502/504) error — a read from the SAME error retries. Each proves the gate is the OPERATION, not the status: the mutation throws after 1 fetch (— mutation not replayed after an ambiguous …) while the read retries and succeeds on 2. Red against the pre-fix ungated code (a mutation replayed to the budget).
node --check green.
Post-Merge Validation
- Watch the
Data Sync Pipeline Run DevIndex Opt-In runs: after a 5xx / gateway blip mid-write, no duplicate addComment / issue-close — the mutation fails loud, not replays.
- Reopen trigger: a duplicated opt-in mutation after a 5xx / gateway.
Out of Scope
- The 403 rate-limit retry (correctly retained — a pre-execution rejection is safe to replay).
- REST-path retries (idempotent GETs).
Authored by Ada (@neo-opus-ada, Claude Opus 4.8, Claude Code). Origin session 3e5f61a5-35d0-4f3d-8805-54f63bebed70.
Resolves #15454
Summary
Surfaced during the #15359 / PR #15419 review — @neo-gpt's RC established that
DevIndex.services.GitHub.query()must not replay a non-idempotent GraphQL mutation after an ambiguous outcome. PR #15419 (merged) closed the two shared-classifier paths (transport catch + 200-body). This closes the two PRE-EXISTING sibling paths #15419's scope deliberately left: the 5xx/403 retry and the in-body 502/504 gateway retry, which still replayed mutations.The fix — extend the retry-authorization gate to all ambiguous paths
Retry classification (is it transient?) and retry authorization (is replay safe?) are separate decisions. A 5xx server error or an in-body 502/504 gateway error leaves a mutation's server-side outcome unknowable — the write (
OptInaddComment/ issue-close) may have applied before the failure — so replaying it can duplicate.>= 500) now fails loud for a mutation via the same#isMutationgate #15419 introduced; a 403 rate-limit is a pre-execution rejection (safe to replay) and stays retryable.query) retry all four paths unchanged.GitHub.query()is now uniformly mutation-safe across all four ambiguous retry paths: transport disconnect + 200-body (#15419) and 5xx + in-body-gateway (this PR).Deltas from ticket
None — the prescribed shape (extend the
#isMutationauthorization gate to the 5xx/gateway paths). The#isMutationhelper + the transport/200-body gates are already on dev via the merged #15419.Evidence: L2 (unit witnesses over both transports' real terminals) → L2 required (a retry-authorization contract; no runtime surface). Residual: none.
Test Evidence
npm run test-unit -- test/playwright/unit/app/devindex/GitHubService.spec.mjs→ 22 passed (2 new discriminating witnesses + the 20 from #15419).query does NOT replay a mutation after a 5xx server error — a read from the SAME 5xx retries+query does NOT replay a mutation after an in-body gateway (502/504) error — a read from the SAME error retries. Each proves the gate is the OPERATION, not the status: the mutation throws after 1 fetch (— mutation not replayed after an ambiguous …) while the read retries and succeeds on 2. Red against the pre-fix ungated code (a mutation replayed to the budget).node --checkgreen.Post-Merge Validation
Data Sync PipelineRun DevIndex Opt-Inruns: after a 5xx / gateway blip mid-write, no duplicateaddComment/ issue-close — the mutation fails loud, not replays.Out of Scope
Authored by Ada (@neo-opus-ada, Claude Opus 4.8, Claude Code). Origin session
3e5f61a5-35d0-4f3d-8805-54f63bebed70.