requestCount is an exact count in every runner, not a lower bound (#573) - #596
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a false-green in the conformance harness: five of the six runners (Go, Python, Ruby, TypeScript, Kotlin) evaluated the requestCount assertion as a lower bound whenever any mock response carried Link: rel="next". That is backwards for the pagination fixtures it covered — they queue more pages than they expect requests because stopping early (at a maxPages/maxItems cap) is the behavior under test, so an over-fetching SDK could pass 3 >= 2. The fix makes requestCount an exact-equality check everywhere, matching what Swift already did in #558. The one fixture where a first-page-only count genuinely doesn't apply ("List operation returns first page with Link header") is now excluded via a link-header tag branch in all six runners, rather than special-casing the assertion.
Changes:
- Replace lower-bound
requestCountlogic with exact equality, extracted into per-language SDK-free helpers (request_count.go,check_request_count,RequestCount,request-count.ts,RequestCount.kt), each with a unit test. - Add a
link-headertag-based skip to the Go, Python, Ruby, and TypeScript runners (Kotlin/Swift already had it). - Restructure SPEC §19 so the
link-headerexclusion is documented once as a shared architectural skip across all six runners.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
conformance/runner/go/main.go |
Adds hasTag + link-header skip loop; delegates requestCount to checkRequestCount. |
conformance/runner/go/request_count.go |
New exact-match checkRequestCount helper. |
conformance/runner/go/request_count_test.go |
Unit tests for the bounds contract. |
conformance/runner/python/runner.py |
Adds check_request_count, tag skip; removes lower-bound branch. |
conformance/runner/python/test_request_count.py |
Unit tests. |
conformance/runner/ruby/runner.rb |
Adds RequestCount module, tag skip; removes lower-bound branch. |
conformance/runner/ruby/request_count_test.rb |
Unit tests. |
conformance/runner/typescript/runner.test.ts |
Adds tag skip; delegates to checkRequestCount; drops hasLinkNextHeader in assertions. |
conformance/runner/typescript/request-count.ts |
New helper. |
conformance/runner/typescript/request-count.test.ts |
Unit tests. |
kotlin/.../conformance/Main.kt |
Delegates requestCount to checkRequestCount. |
kotlin/.../conformance/RequestCount.kt |
New helper. |
kotlin/.../conformance/RequestCountTest.kt |
Unit tests. |
SPEC.md |
Documents the shared link-header architectural skip across all six runners. |
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1e6f840 to
784c15b
Compare
c239c0c to
a378d89
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (1)
conformance/runner/go/main.go:210
- The new
hasTagfunction was inserted between the// Tests where the Go SDK's behavior intentionally differs.comment and thegoSDKSkipsdeclaration it documents. As a result, that comment now reads ashasTag's doc comment (in Go, a comment immediately preceding a declaration is its godoc), whilegoSDKSkipsis left undocumented. MovinghasTagabove the comment keeps the comment attached togoSDKSkips.
// Tests where the Go SDK's behavior intentionally differs.
// hasTag reports whether the fixture carries the given tag.
func hasTag(tc TestCase, tag string) bool {
784c15b to
46992cb
Compare
a378d89 to
9d4cf25
Compare
46992cb to
46eb2aa
Compare
9d4cf25 to
11ae7e4
Compare
46eb2aa to
13e6bae
Compare
11ae7e4 to
bdae816
Compare
13e6bae to
4656e78
Compare
bdae816 to
806ff90
Compare
1ff2025 to
40099b1
Compare
d67f706 to
211acbc
Compare
40099b1 to
2bbc7a1
Compare
211acbc to
2b63ad2
Compare
2bbc7a1 to
db00e40
Compare
2b63ad2 to
b398be3
Compare
db00e40 to
9e5eb36
Compare
Go, Python, Ruby, TypeScript and Kotlin all evaluated the `requestCount`
assertion as a LOWER bound whenever any mock response carried
`Link: rel="next"`:
if autoPaginates {
if requestCount < expected { fail }
} else if requestCount != expected { fail }
That is backwards for the fixtures it covered. conformance/tests/pagination.json
queues MORE pages than it expects requests in two cases, because stopping early
is the behavior under test:
Pagination stops at maxPages safety cap 3 pages queued, 2 expected
maxItems caps results across pages 3 pages queued, 2 expected
Auto-pagination follows Link headers ... 3 pages queued, 3 expected
An SDK that ignored the cap and walked all three passed `3 >= 2`. The third is
the exposed case: its only assertions are requestCount and noError, so an
over-fetch had nothing else to catch it — the other two happen to carry a
`responseMeta` truncated assertion that fires instead, coverage by luck.
The relaxation was protecting exactly one fixture, "List operation returns
first page with Link header" (tagged `link-header`), whose `requestCount: 1`
counts FIRST-PAGE requests only and so cannot apply to an auto-paginating SDK.
With that one fixture's count taken out of scope, nothing reaching the
evaluator needs a lower bound, and `>=` becomes `!=` everywhere. Swift took
this in #558 and its shape is what the five now match. The
MockEngine/httptest/MSW/respx/WebMock auto-pagination tolerance stays:
answering an over-walk with a terminal empty page rather than an error is what
lets the tightened assertion report a clean count mismatch.
Each runner's predicate moves into that language's SDK-free support module
alongside `delay_gaps` — request_count.go, `check_request_count` in runner.py,
`RequestCount` in runner.rb, request-count.ts, RequestCount.kt — with a unit
test per language. Those five test files are discovered automatically thanks to
#572; before it, three of them would have been executed by nothing.
Take the ASSERTION out of scope, not the CASE
-----------------------------------------------------------------------
An earlier revision of this commit did it the blunt way: it skipped the whole
`link-header` CASE in Go, Python, Ruby and TypeScript, copying what Kotlin and
Swift already did. That is wrong, and wrong in this stack's own signature
shape.
The fixture carries THREE assertions, not one:
requestCount: 1 inapplicable — the SDK follows the Link header
statusCode: 200 perfectly applicable
noError perfectly applicable
Kotlin and Swift had always skipped the case, so the moment the other four
joined them the fixture was skipped by ALL SIX. Its `statusCode` and `noError`
assertions had been running in four runners; they then ran in zero. And nothing
reports that: the fixture still sits in `conformance/tests/pagination.json`,
still passes `conformance-fixtures-check` and `check-fixture-coverage`, so the
build stays green over a fixture no runner executes. That is precisely #572's
defect — present in the tree, run by nothing — one layer down, committed inside
the stack that exists to close it.
So the exclusion is now one assertion wide. `requestCountApplies(tags)` (and
its per-language spellings) returns false for `link-header`, the evaluator
skips that one assertion, and the case runs. Verbatim before/after from the
TypeScript runner, which is the arm where a skip is directly observable —
pre-narrowing files taken from this stack's pushed head a378d89, everything
else identical:
$ npx vitest run --reporter=verbose -t "List operation returns first page with Link header"
↓ runner.test.ts > conformance/pagination.json > List operation returns first page with Link header (TS SDK auto-paginates; follows Link headers by design)
[...]
Tests 182 skipped (182)
REAL_EXIT=0
$ npx vitest run --reporter=verbose -t "List operation returns first page with Link header"
✓ runner.test.ts > conformance/pagination.json > List operation returns first page with Link header 19ms
[...]
Tests 1 passed | 184 skipped (185)
REAL_EXIT=0
([...] elides vitest's per-file listing of the other 180-odd cases the `-t`
filter skipped, and its Test Files / Duration lines. The two quoted lines and
the Tests summary are verbatim. Zero passed becomes one passed: the case was
executed by nothing and now runs.)
Kotlin and Swift keep the whole-case skip, deliberately
-----------------------------------------------------------------------
They are not narrowed, and this is a stated exception rather than an oversight.
Both derive the response status from the last mock response the SDK consumed,
and an auto-paginating SDK walks past the end of a one-response queue, so
`statusCode` has nothing to report. Narrowing them was tried; both fail.
Verbatim, one line each from the pagination.json section plus the run tail:
$ make conformance-kotlin
FAIL: List operation returns first page with Link header
Expected status code 200, but got no response
[...]
Passed: 142, Failed: 1, Skipped: 0, Total: 143
REAL_EXIT=2
$ make conformance-swift
FAIL: List operation returns first page with Link header
Expected status code 200, but got no response
[...]
Passed: 142, Failed: 1, Skipped: 0, Total: 143
REAL_EXIT=2
([...] elides the other 142 result lines of each run. Note the exit code:
through `make` a recipe failure is 2, not the runner binary's own 1.)
Widening those two runners' status model is separate work, not a skip to delete
here. Both call sites now carry that reasoning in a comment, so the asymmetry
does not read as drift to be "aligned" away.
Net effect on coverage: the fixture goes from run by four runners (before this
stack) to run by four runners (after), instead of by zero. THIS COMMIT CHANGES
NO SKIP COUNT IN ANY RUNNER — see the identical figures in the two verification
blocks below.
The general gap remains: nothing in the build detects a fixture that every
runner skips. That is filed as #602 and referenced from SPEC §19; this commit
fixes the instance, not the class.
Red proof 1 — the runners on the real fixture
-----------------------------------------------------------------------
Mutating "Auto-pagination follows Link headers across multiple pages" to expect
2 while the SDK makes 3 is exactly the over-fetch the lower bound waves
through. Base is 07482b9ba, this stack's #553 tip, byte-identical to
origin/main for every runner file involved. Each line is the fixture's own
result line plus that command's real exit status:
$ make conformance-<lang> # base 07482b9ba (lower bound)
PASS: Auto-pagination follows Link headers across multiple pages
REAL_EXIT[base-conformance-go]=0
REAL_EXIT[base-conformance-python]=0
REAL_EXIT[base-conformance-ruby]=0
REAL_EXIT[base-conformance-typescript]=0
REAL_EXIT[base-conformance-kotlin]=0
$ make conformance-<lang> # this commit (exact count)
FAIL: Auto-pagination follows Link headers across multiple pages
Expected 2 requests, got 3
REAL_EXIT[branch-conformance-go]=2
REAL_EXIT[branch-conformance-python]=2
REAL_EXIT[branch-conformance-ruby]=2
REAL_EXIT[branch-conformance-typescript]=2
REAL_EXIT[branch-conformance-kotlin]=2
(The PASS/FAIL lines above are one per language and identical in each; TS
prints its as `FAIL runner.test.ts > conformance/pagination.json >
Auto-pagination follows Link headers across multiple pages` with `Error:
[Auto-pagination follows Link headers across multiple pages] Expected 2
requests, got 3`. Every exit code is real and from `make`, which reports a
recipe failure as 2. The fixture edit was reverted; pagination.json is
unchanged in this commit.)
Red proof 2 — the new unit tests against the old predicate
-----------------------------------------------------------------------
Restoring the lower bound in each support module fails the same three cases in
every language. These are the bare test binaries, so the exit code is 1, not
make's 2:
Go --- FAIL: TestRequestCountRejectsAnOverFetch
--- FAIL: TestRequestCountMessageNamesBothCounts
--- FAIL: TestRequestCountZeroExpectedRequiresZeroActual
FAIL github.com/basecamp/basecamp-sdk/conformance/runner/go REAL_EXIT=1
Python 3 failed, 6 passed in 0.10s REAL_EXIT=1
Ruby 9 runs, 13 assertions, 3 failures, 0 errors, 0 skips REAL_EXIT=1
TS Tests 3 failed | 6 passed (9) REAL_EXIT=1
Kotlin 23 tests completed, 3 failed (RequestCountTest: 6 tests,
3 failures — "an over-fetch fails", "the failure message names
both counts", "zero expected requires zero actual") REAL_EXIT=1
The three new `requestCountApplies` tests per language are not part of that
red: they pin the SCOPE of the exclusion, asserting that a `link-header`
fixture keeps its statusCode and noError assertions live. If the suppression
ever widens back to the whole case, those fail.
Verification, exact-count code, real exit codes, measured on this commit
-----------------------------------------------------------------------
make conformance-go Passed: 141, Failed: 0, Skipped: 2 REAL_EXIT=0
make conformance-python 143 passed, 0 failed, 0 skipped REAL_EXIT=0
make conformance-ruby 132 passed, 0 failed, 11 skipped REAL_EXIT=0
make conformance-typescript 183 passed | 2 skipped (185) REAL_EXIT=0
make conformance-kotlin Passed: 142, Failed: 0, Skipped: 1 REAL_EXIT=0
make conformance-swift Passed: 142, Failed: 0, Skipped: 1 REAL_EXIT=0
make conformance-runner-tests-go ok (cached) REAL_EXIT=0
make conformance-runner-tests-python 29 passed, 31 subtests REAL_EXIT=0
make conformance-runner-tests-ruby 11 + 6 + 9 runs, 0 fail REAL_EXIT=0
make conformance-runner-tests-kotlin (--quiet, no output) REAL_EXIT=0
make conformance-runner-tests-swift 39 tests, 0 failures REAL_EXIT=0
./scripts/check-runner-test-reachability 9 checks passed REAL_EXIT=0
./scripts/check-runner-test-reachability --self-test 6 cases REAL_EXIT=0
./scripts/check-replay-decoder-parity 5 checks, 31 operations REAL_EXIT=0
cd conformance/runner/go && go build ./... && go vet ./... REAL_EXIT=0
make lint-actions No findings to report REAL_EXIT=0
And the same six suites at base 07482b9ba, for the skip-count comparison — the
figures the "no skip count changes" claim above rests on:
base conformance-go Passed: 141, Failed: 0, Skipped: 2
base conformance-python 143 passed, 0 failed, 0 skipped
base conformance-ruby 132 passed, 0 failed, 11 skipped
base conformance-typescript 174 passed | 2 skipped (176)
base conformance-kotlin Passed: 142, Failed: 0, Skipped: 1
Identical skip counts throughout. TypeScript's total rises 176 → 185 because
this commit adds request-count.test.ts's 9 tests; its SKIPPED count is 2 on
both sides. (Those base figures come from the red-proof-1 base runs, which
carry the mutated expectation — a changed expectation moves no skip.)
The Go runner's `hasTag` helper is deleted. It existed only for the whole-case
branch this commit no longer has, and it had been inserted between
`goSDKSkips`' doc comment and `goSDKSkips` itself, so godoc read that comment
as documenting `hasTag`.
SPEC §19: the `link-header` entry is rewritten. Previously it was a per-runner
repeat that claimed Swift's skip was "identical to Kotlin and TypeScript" —
untrue, TypeScript had no `link-header` handling at all. It is now one block
that states what each runner excludes, why Kotlin and Swift differ, and what
the all-six-skipped shape would cost.
Local figures. Cite the CI job's own numbers when they differ.
Closes #573
b398be3 to
68a70c2
Compare
…743) * Detect a fixture case that every runner excludes (#602) Each runner's case census (#742) answers "did THIS runner account for every case". It cannot answer the question #602 actually asks — is any case executed by NO runner — because a case every runner excludes leaves all six censuses green: each one counted its own skip. Only a comparison across runners sees it. Every runner now writes the cases it did not execute, with reasons, to conformance/manifests/<runner>.json, and scripts/check-fixture-execution.rb fails when a case appears in all six. Manifests rather than parsed output because TypeScript prints no `SKIP:` line — a skip there is `it.skip`, reported in vitest's own format — so a gate scraping stdout would be blind to exactly one runner, in the silent direction: TypeScript would contribute an empty exclusion set and no case could reach all-six. Each manifest also records `executed`, asserted against the census total by both writer and reader: without it a case a runner silently dropped is simply absent from its exclusion set, and absent reads identically to "ran fine". THE ABSENCE RULE IS THE DESIGN. FULL mode requires all six manifests and fails if any is missing — a missing manifest must never read as "that runner executed everything", which is exactly what makes an all-six case invisible. Swift's runner is macOS-only, so a Linux run produces five and uses PARTIAL mode: an exclusion shared by every VISIBLE runner is a warning, never a failure, because five-of-six is not the all-six claim and a warning cannot false-fail. Both modes fail on zero manifests. CI resolves it properly rather than living with the partial answer: the six language jobs each upload their manifest and the existing fan-in job runs FULL mode over all six. That step is ordered AFTER the results check, because the job is `if: always()` — a language job that died before its upload would otherwise be reported as a missing manifest, burying the real cause. The local target depends on `conformance` rather than trusting whatever manifests are on disk, which would let it validate last week's exclusion sets. Maximum overlap today is 2 of 6 (#596 narrowed it), so the gate is green on arrival and a live run only proves it can say yes. scripts/test-check-fixture-execution.rb crafts the all-six state and every absence, integrity and disagreement case; it already found one defect (a non-object manifest crashed with a backtrace instead of naming the file). Verified end-to-end too: adding one shared exclusion to all six real manifests makes the gate fail and name every runner's reason. SPEC §19 now describes the gate that exists, closing the gap #740's P1 found — where the prose promised `make check-fixture-execution` after the source-text parser it referred to had been withdrawn. * Gate the Ruby and Python manifest uploads to the leg that runs conformance Both jobs are matrixed and run their conformance suite on ONE version — `matrix.ruby == '3.3'`, `matrix.python == '3.13'`. The upload steps went in unconditionally, so on every other leg the conformance step was skipped, no manifest was written, and `if-no-files-found: error` failed the job. Seven red checks, all of them this. Mirroring the condition is the fix rather than relaxing if-no-files-found: a leg that DOES run conformance and produces no manifest is a real defect, and that is exactly what the fan-in gate's absence rule depends on being loud. Go, TypeScript, Kotlin and Swift have no matrix, so their uploads stay unconditional and each artifact name remains unique. * Keep --partial from softening a complete manifest set, and wire the target in Two review findings from Copilot, both correct. `--partial` describes the INPUT — "this run cannot produce all six" — not a licence to soften the verdict. When every expected runner reported anyway (a macOS developer passing the flag out of habit, or a CI step keeping it for safety), "excluded by all present runners" IS the all-six claim, and the warning path let the one state this gate exists to reject exit 0. Partial handling now applies only when a manifest is genuinely absent. The self-test case for it was shown to fail against the un-narrowed code first. `check-fixture-execution` was also absent from `check-targets`: the edit that was supposed to add it matched the first occurrence of its anchor, which was the .PHONY line, so the target existed and `make check` never ran it. Now in the list, verified by reading the check-targets line itself rather than grepping the file. * Give exclusions a real identity, own a fresh run, and self-test in CI Four review findings, all real, two of them defects in the gate's core claim. CASE IDENTITY IS [file, name], NOT name. Codex is right that fixture case names are not unique: verified, "replace-omission-clears: sparse replace sends the request verbatim with no GET" appears in THREE fixtures and the non-idempotent POST retry name in two, while names are unique within a file. Keyed on name alone, a runner excluding one of those collapsed two entries into one — its own `executed + excluded` integrity check would then fail spuriously, and worse, a name excluded by three runners in one file and three in another would read as excluded by all six. A false failure on cases that all run. Every runner now records the fixture file, and the gate keys on the pair; it also rejects the same case excluded twice by one runner, which would otherwise add up while the comparison saw a single entry. STALE MANIFESTS. Both bots found this, and it was created by the --partial narrowing in the previous commit. On Linux `conformance-swift` is a no-op, so a swift.json left by an earlier macOS run over the same checkout survives while the other five refresh; the gate then sees six manifests, stops treating the run as partial, and compares five current exclusion sets against a stale sixth. Silent-wrong, in a gate whose whole claim is about what the runners actually did. The target now wipes conformance/manifests and runs the suite itself, so "six manifests present" means "six runners reported in this run". The reset is in the recipe, not a prerequisite: prerequisite order is not guaranteed under `make -j`, and a reset racing the runners would delete the output it exists to protect. check-targets therefore lists this target INSTEAD of `conformance`, so the suite still runs exactly once per `make check`. SELF-TEST IN CI. The fan-in job ran only the live gate, whose inputs are built to pass; the self-test was reachable only through the Make target the workflow deliberately bypasses. A regression making the gate accept a missing manifest or an all-six exclusion would have left CI green — the check-targets-is-not-CI -coverage shape, in the PR that adds the gate. It now runs as its own step. New self-test cases: one name in two files is two cases and must NOT fire; the same name in one file excluded everywhere still must; an exclusion without its fixture file is rejected; one case excluded twice in a manifest is rejected. * Reset manifests via an order-only prerequisite, not a sub-make The previous commit had check-fixture-execution run `$(MAKE) conformance` itself so it could wipe the manifest directory first. That worked, and cost something I did not price: with `conformance` no longer listed in check-targets, `conformance-kotlin` stopped being reachable from check-targets in the graph check-gradle-serialization walks — so that gate could no longer see one of the two Gradle invocations it exists to keep off each other. Its self-test caught it exactly as designed ("conformance-kotlin edge removed: expected the gate to FAIL, it passed"), which is a mutation case earning its keep on a change nobody wrote it for. Restructured so both properties hold. `conformance` is back in check-targets and check-fixture-execution depends on it, restoring reachability. The reset is now a phony target that all six language targets take as an ORDER-ONLY prerequisite (`|`), which is what makes it correct under `make -j`: make builds a prerequisite to completion before any dependent starts, and builds the phony target once per invocation, so the reset cannot race the runners it protects. A plain prerequisite of the aggregate target would not do that — siblings may run concurrently. It fires for a single-language run too, deliberately: `make conformance-go` clears the directory, so it holds only what that invocation produced and the gate goes partial rather than silently mixing runs across machines. Verified both directions: a planted stale swift.json is wiped and regenerated, and `make conformance-go` alone leaves only go.json. * Let a re-run replace its own manifest artifact Copilot flagged this on all six uploads. Re-running a job creates a new ATTEMPT within the same run, and upload-artifact v4+ artifacts are immutable per run — so the second attempt fails on a name conflict with the first attempt's manifest, before the fan-in gate can run. Reachable exactly when someone is re-running to get a red PR green, which is the worst time for the gate to become unreachable. `overwrite: true` is also right on the merits, not just as conflict avoidance: the collecting gate must read THIS attempt's exclusion set. An artifact left by a previous attempt is the CI-side version of the stale-manifest bug fixed two commits ago, and the same answer applies — six manifests present must mean six runners reported in this run.
Closes #573. Third of a three-PR stack. Stacked on #595 (which is stacked on #594) — merge in order; the five unit tests added here are discovered automatically because of #594. Review this PR's own commit only.
The defect
Go, Python, Ruby, TypeScript and Kotlin all evaluated the
requestCountassertion as a lower bound whenever any mock response carriedLink: rel="next":That is backwards for the fixtures it covered.
conformance/tests/pagination.jsonqueues more pages than it expects requests in two cases, because stopping early is the behavior under test:An SDK that ignored the cap and walked all three passed
3 >= 2. The third is the exposed case: its only assertions arerequestCountandnoError, so an over-fetch had nothing else to catch it — the other two happen to carry aresponseMetatruncated assertion that fires instead, coverage by luck.The fix
The relaxation was protecting exactly one fixture, "List operation returns first page with Link header" (tagged
link-header), whoserequestCount: 1counts first-page requests only and so cannot apply to an auto-paginating SDK. With that one fixture's count taken out of scope, nothing reaching the evaluator needs a lower bound, and>=becomes!=everywhere. Swift took this in #558 and its shape is what the five now match.The MockEngine / httptest / MSW / respx / WebMock auto-pagination tolerance stays: answering an over-walk with a terminal empty page rather than an error is what lets the tightened assertion report a clean count mismatch instead of an opaque transport error.
Each predicate moves into that language's SDK-free support module alongside
delay_gaps—request_count.go,check_request_countinrunner.py,RequestCountinrunner.rb,request-count.ts,RequestCount.kt— with a unit test per language. Before #594, three of those five test files would have been executed by nothing.Take the ASSERTION out of scope, not the CASE
This is the review finding that reshaped the PR. An earlier revision did it the blunt way: skip the whole
link-headercase in Go, Python, Ruby and TypeScript, copying what Kotlin and Swift already did. That is wrong, and wrong in this stack's own signature shape.The fixture carries three assertions, not one:
requestCount: 1statusCode: 200noErrorKotlin and Swift had always skipped the case, so the moment the other four joined them the fixture was skipped by all six. Its
statusCodeandnoErrorassertions had been running in four runners; they then ran in zero. And nothing reports that: the fixture still sits inconformance/tests/pagination.json, still passesconformance-fixtures-checkandcheck-fixture-coverage, so the build stays green over a fixture no runner executes. That is precisely #572's defect — present in the tree, run by nothing — one layer down, committed inside the stack that exists to close it.So the exclusion is now one assertion wide.
requestCountApplies(tags)(and its per-language spellings) returns false forlink-header, the evaluator skips that one assertion, and the case runs.Verbatim before/after from the TypeScript runner, the arm where a skip is directly observable. Pre-narrowing files taken from this stack's earlier pushed head
a378d8987, everything else identical:[...]elides vitest's per-file listing of the other 180-odd cases the-tfilter skipped, plus its Test Files / Duration lines. The two quoted lines and the Tests summaries are verbatim. Zero passed becomes one passed: the case was executed by nothing and now runs.Kotlin and Swift keep the whole-case skip, deliberately
They are not narrowed, and this is a stated exception rather than an oversight. Both derive the response status from the last mock response the SDK consumed, and an auto-paginating SDK walks past the end of a one-response queue, so
statusCodehas nothing to report. Narrowing them was tried; both fail:[...]elides the other 142 result lines of each run. Note the exit code: throughmakea recipe failure is 2, not the runner binary's own 1.Widening those two runners' status model is separate work, not a skip to delete here. Both call sites now carry that reasoning in a comment, so the asymmetry does not read as drift to be "aligned" away.
Net effect on coverage: the fixture goes from run by four runners (before this stack) to run by four runners (after), instead of by zero. This PR changes no skip count in any runner — see the two verification blocks below.
The general gap remains: nothing in the build detects a fixture that every runner skips. Filed as #602 and referenced from SPEC §19. This PR fixes the instance, not the class.
Red proof 1 — the runners, on the real fixture
Mutating "Auto-pagination follows Link headers across multiple pages" to expect 2 while the SDK makes 3 is exactly the over-fetch the lower bound waves through. Base is
07482b9ba, this stack's #553 tip, byte-identical toorigin/mainfor every runner file involved.The PASS/FAIL lines are one per language and identical in each; TS prints its as
FAIL runner.test.ts > conformance/pagination.json > Auto-pagination follows Link headers across multiple pageswithError: [Auto-pagination follows Link headers across multiple pages] Expected 2 requests, got 3. Every exit code is real and frommake, which reports a recipe failure as 2 — an earlier revision of this PR reported these as 1, which is the bare runner binary's code, not the one the stated command produces.The fixture edit was reverted;
conformance/tests/pagination.jsonis unchanged in this PR.Red proof 2 — the new unit tests, against the old predicate
Restoring the lower bound in each support module fails the same three cases in every language. These are the bare test binaries, so the exit code is 1, not make's 2:
The three new
requestCountAppliestests per language are not part of that red: they pin the SCOPE of the exclusion, asserting that alink-headerfixture keeps itsstatusCodeandnoErrorassertions live. If the suppression ever widens back to the whole case, those fail.Verification, exact-count code (real exit codes, measured on this commit)
And the same suites at base
07482b9ba— the figures the "no skip count changes" claim rests on:Identical skip counts throughout. TypeScript's total rises 176 → 185 because this PR adds
request-count.test.ts's 9 tests; its skipped count is 2 on both sides. (Those base figures come from the red-proof-1 base runs, which carry the mutated expectation — a changed expectation moves no skip.)Local figures — cite the CI job's own numbers where they differ.
Also in this PR
The Go runner's
hasTaghelper is deleted (thread). It existed only for the whole-case branch this PR no longer has, and it had been inserted betweengoSDKSkips' doc comment andgoSDKSkipsitself, so godoc read that comment as documentinghasTag.Docs
SPEC §19's
link-headerentry is rewritten. Previously it was a per-runner repeat that claimed Swift's skip was "identical to Kotlin and TypeScript" — untrue, TypeScript had nolink-headerhandling at all. It is now one block stating what each runner excludes, why Kotlin and Swift differ, and what the all-six-skipped shape would cost.No spec files touched (
spec/basecamp.smithy,openapi.jsonunchanged).