Let the conformance suite state a retry cap and pin the one-attempt floor
The gap
conformance/schema.json configOverrides carried only baseUrl, maxPages and maxItems, and every runner built its client on the default cap of 3. The suite could not say "max_retries: 0 sends exactly one request", so the one-attempt floor Go gained in #180 and Ruby in #181 was pinned by a unit test in its own language and nowhere else — and whether the other SDKs floor at all was unstated.
The precedent
basecamp-sdk pins it: conformance/tests/retry.json "max_retries 0 sends exactly one request on a governed operation" — configOverrides.maxRetries: 0, a single queued 503, requestCount: 1 — with each runner routing the override to its SDK's own spelling of the cap, and the fixture description recording which runners pre-check the budget (a real guard) versus post-check it (0 and 1 indistinguishable). This copies the shape. The basecamp-sdk companion case on ungoverned download traffic has no fizzy analogue: every one of the 112 modelled operations carries a retry block, so there is no ungoverned path to reach.
What lands
- Schema:
configOverrides.maxRetries, integer ≥ 0, documented as a TOTAL attempt count. - Fixture: one queued 503 on
GetBoard(modelled budget 3,retry_on[429, 500, 503]),requestCount: 1,statusCode: 503. Every runner answers past the end of its queue with a 500, which is also in the declared set, so an SDK that retried keeps going to its cap and the count cannot pass vacuously. - Runners: Go
WithMaxRetries(pointer field so 0 survives), Rubymax_retries, KotlinmaxRetries, Rustmax_attempts(Option<u32>so 0 survives, plus a serde test) take the number. TypeScript exposes no numeric cap — its retry middleware is driven by the per-operationretry.max— so a cap of 0 or 1 maps toenableRetry: falseand anything above totrue, the cutoff being> 1because 0 and 1 both mean one attempt.
Results, all run locally on this branch
| Runner | Result | Floor |
|---|---|---|
| Go | 128 passed, 0 failed | pre-check loop, floored since #180 — real guard |
| TypeScript | 128 passed, 0 failed | post-check (middleware); no numeric cap, override → enableRetry: false |
| Ruby | 128 passed, 0 failed | pre-check loop, floored since #181 (merged) — real guard |
| Rust | 128 passed, 0 failed; runner tests 19 passed | floored in the builder (max_attempts(0) reads as one), route budget clamped under it |
| Kotlin | 128 passed, 0 failed on this head (stacked on #182; a compile error against main alone) | post-check; client cap is a ceiling on the operation budget only with #182 |
| Swift | no runner exists | SDK loop is max(enableRetry ? maxAttempts : 1, 1) — floored, unpinned by conformance |
Stacked on #182
The Kotlin SDK's builder DSL never exposed maxRetries, and FizzyHttpClient sized its budget as opRetry?.maxRetries ?: config.maxRetries — every operation has a modelled budget, so the client cap was dead for governed traffic. Both are fixed in #182, following the brief's rule that an SDK that does not honour the cap is its own fix. This PR's base is #182's branch so the Kotlin runner compiles and the suite is green at the head CI builds; GitHub retargets it to main when #182 merges.
Not in this PR
Swift: Makefile's conformance-swift target checks for conformance/runner/swift/Package.swift and prints SKIP, and CI's test.yml has no Swift conformance job. Building a runner from nothing is not cap plumbing; the SDK's own loop already floors at one attempt. Flagged rather than done.