Skip to content

feat(corpus): fetch the API documents the tests read, rather than committing them - #131

Merged
ShocOne merged 1 commit into
mainfrom
feat/corpus-fixtures
Aug 7, 2026
Merged

feat(corpus): fetch the API documents the tests read, rather than committing them#131
ShocOne merged 1 commit into
mainfrom
feat/corpus-fixtures

Conversation

@ShocOne

@ShocOne ShocOne commented Aug 7, 2026

Copy link
Copy Markdown
Member

First step of removing per-API-product artefacts from this repo. Touches no existing test — nothing reads this package yet, so main cannot move.

Why

95% of this repository's tracked files are artefacts of specific API products (pilot/ 4,673 files, recordings/ 1,340, blueprints/ 203, openapi/ 7). The toolkit itself is 280. Those artefacts belong in the provider repos the pipeline generates.

But the tests still need real documents. Every inference defect this generator has had was found by a second vendor's specification disagreeing with the first — not by a synthetic fixture that agrees with the code. So documents are fetched at test time and pinned, rather than vendored.

What

internal/corpus wraps the existing internal/snapshot rather than reimplementing pinning. Snapshot already stores documents immutably with a checksum and self-describing metadata; the only thing it lacked as a cache was a directory name derived from the pin rather than the wall clock, so a pinned document can be found again instead of merely being present.

internal/corpus/corpus.lock.json is the reviewable artefact — version, SHA-256, path and operation counts per document. A fetch matching none of them fails rather than proceeding.

That is the point rather than a detail. Tests assert facts about these documents — enum members, candidate counts, inferred bindings — so a vendor editing a specification changes what those tests mean. The lock converts an inexplicable failure weeks later into a deliberate review.

New CLI: corpus sync | check | refresh, documented in docs/cli.md.

Three decisions, each with a failure mode behind it

Staged then renamed. go test ./... runs packages as separate processes, so several race to fill a cold cache, and snapshot.Pin refuses to overwrite — immutability is the property it defends. Without atomic publication the first run after any lock change fails spuriously in whichever packages lost the race. That would present as flakiness and be diagnosed as anything but this.

Skip locally, fail in CI. Failing everywhere turns a developer's flaky connection into a red suite, which teaches people to ignore red. Skipping everywhere lets CI skip its way to green, which is a standing lie about coverage. A document that was fetched but did not match its pin is never skipped anywhere.

Mirror preferred over upstream — and this is the part that stopped being hypothetical while I was writing it.

ThousandEyes had already moved

The first cold sync failed:

the pinned thousandeyes document is not what https://pubhub.devnetcloud.com/.../api.yaml served.
  pinned  sha256:969c3dfceafe  version 7.0.98  201 path(s) / 315 operation(s)
  fetched sha256:664dffebf5db  version 7.0.99  205 path(s) / 323 operation(s)

That URL serves current, not a version. 7.0.98 — the snapshot committed in this repo — is no longer fetchable from anywhere. GitHub's description has the same shape of problem, published from main.

So the pin here is 7.0.99, taken deliberately through corpus refresh, and refresh warns on every pin lacking a mirror. A mirror is a prerequisite for this scheme, not a nicety — without one, refreshes are forced on the vendor's schedule. A mirror cannot weaken anything, since the digest is checked identically whichever source answered; it is a CDN, not a source of truth. None is configured yet — that needs a decision on where it lives.

Verification

$ tfpfgen corpus check
✅ github         unchanged: version 1.1.4 (sha256:80850db290cd, 808 paths, 1220 operations)
✅ jamfpro        unchanged: version production (sha256:e9a22a2d4af4, 547 paths, 817 operations)
📌 thousandeyes   upstream has moved
  • Cold sync materialises all three in 3.7s; warm sync is 0.44s and makes no request.
  • Seven hermetic tests (httptest, no network): lock integrity, cold-fetches-warm-does-not with a request counter, the mismatch message, recovery from a corrupted cache, mirror-preferred and mirror-cannot-change-meaning, offline-is-not-corruption, cache redirection.
  • go build ./..., go vet ./..., go test ./..., gofumpt all clean. .tfpfgen/cache/ is already gitignored — verified with git check-ignore.

🤖 Generated with Claude Code

…mitting them

This repository generates Terraform providers; it is not the home of any
vendor's artefacts, and today 95% of its tracked files are exactly that. The
documents have to come from somewhere, though: inference bugs are found by
specifications that disagree with each other, and every one this generator has
had was found by a second vendor rather than by a synthetic fixture that agreed
with the code. So the documents are fetched at test time and pinned here.

Pinned means pinned. corpus.lock.json records a version, a SHA-256 and the path
and operation counts, and a fetch matching none of them fails rather than
proceeding quietly. That is the whole point rather than a detail: tests assert
facts about these documents -- enum members, candidate counts, inferred bindings
-- so a vendor editing a specification changes what those tests mean. The lock
turns that from an inexplicable failure weeks later into a review taken
deliberately with corpus refresh.

Nothing here reimplements pinning. internal/snapshot already stores documents
immutably with a checksum and self-describing metadata; this adds the one thing
it lacked for use as a cache, a directory name derived from the pin rather than
from the wall clock, so a pinned document can be found again instead of merely
being present.

Three decisions worth stating, because each has a failure mode behind it.

Documents are materialised into a staging directory and renamed into place. `go
test ./...` runs packages as separate processes, so several race to fill a cold
cache, and snapshot.Pin refuses to overwrite -- immutability is the property it
exists to defend. Without atomic publication the first run after any lock change
fails spuriously in whichever packages lost the race, which would look like
flakiness and be diagnosed as anything but this.

An unfetchable document skips locally and fails when TFPFGEN_CORPUS_REQUIRED is
set. Failing everywhere would turn a developer's flaky connection into a red
suite and teach people to ignore red; skipping everywhere would let CI skip its
way to green, which is a standing lie about coverage. A document that was
fetched but did not match its pin is never skipped anywhere.

Fetching prefers a mirror over upstream, and this is not hypothetical
future-proofing. Writing this commit, ThousandEyes had already moved: the pinned
7.0.98 was gone from the unified-oas path, which serves current rather than a
version, and 7.0.99 was in its place. GitHub's description has the same shape of
problem, published from main. A mirror holding the exact pinned bytes cannot
weaken anything, because the digest is checked identically whichever source
answered -- it is a CDN, not a source of truth -- and without one every refresh
is forced on the vendor's schedule. No mirror is configured yet; refresh warns
about each pin that lacks one.

The ThousandEyes pin is 7.0.99 for that reason: 7.0.98 is no longer fetchable
from anywhere.

Touches no existing test. Nothing reads this package yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShocOne
ShocOne merged commit 9cc2149 into main Aug 7, 2026
10 of 12 checks passed
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.

1 participant