feat(conformance): seed-and-settle foundation, Steps 4b-5a (S08 remote, batch 1b/12)
Summary
Related to #47 (closed).
Band A foundation batch per the S08 remote goal-run's annex §7 MR clusters
("Band A foundation"), second half. Lands Steps 4b, 5 and 5a: the derived
--timeout, the remotefake caching-proxy double, and the shared
seed-and-settle helper. This completes Band A.
!258 (merged) has merged, so this targets main directly and the diff is just Steps
4b, 5 and 5a: 16 files, 7,879 insertions.
Brought up to date by merge, not rebase (4cd274e). !261 (merged)'s
sshen/npm-remote-preflight-batch branches off 9481baa in this branch's
history, so a rebase would orphan its base. The ten-file conflict came from
!258 (merged) being squash-merged: main carries its last ten commits as one commit git
cannot relate to the Band A commits already merged here. 4cd274e's message
has the per-file resolution. An earlier revision of this section said the
branch would rebase cleanly onto main; it did not.
Annex §7 plans cluster 1 as a single MR covering all seven Band A steps. It
ships as two because the Maven band (Steps 6-14) was reassigned and that band's
critical path runs through Band A: Step 7 depends on Steps 2 and 3, Step 8 on
Steps 2 and 4a, so !258 (merged) unblocks both a review cycle earlier than a
seven-step MR would. This MR is what unblocks Maven Step 9 onward, which needs
Steps 5 and 5a. Recorded in validation/deviations.md D4.
Size
16 files, 7,879 insertions: 2,377 lines of production Go against 5,332 test Go, so ~69% test volume. Stated up front rather than left for a reviewer to find.
The three steps are batched for sequencing, not because they cannot separate.
They share no file, no import edge and no test edge in this MR: nothing imports
pkg/conformance/internal/remotefake, seed_test.go runs entirely off stub
SeedCallbacks with no format and no HTTP server, and Step 4b's internal/cli
code references neither SeedAndSettle nor remotefake. The coupling the
batch is named for begins at Steps 9, 19 and 30, which call the helper and
drive the double together. What batching buys here is one review cycle instead
of three on a critical path three bands are waiting on. An earlier revision of
this section claimed the steps could not separate further, which the import
graph does not support.
Steps
-
Step 4b: the derived
--timeout.internal/cli/{flags,run,stderr}.goandREADME.md, with three test files. Covers AC #30 (closed), #31 (closed), #32, #34 and AC #2 (closed)'s exit-code half. No catalog rows.Drops
newTimeoutFlag's registered2m, derives2n × d + 5munder a resolved remote kind, reordersrunActionso selection and the default resolution precedecontext.WithTimeout(which still precedesparseCredential), and renders S08 §Seeding's four expiry messages. All four strings are taken verbatim from §Seeding; the plan reproduces none of them on purpose.README.md's--timeoutrow said "Default2m", which is wrong for a remote run by a factor of eight on OCI. Dropping the registered default removes the only other channel an operator sizing a CI job's wall clock could read it from. -
Step 5:
remotefake, the caching-proxy double.pkg/conformance/internal/remotefake/. No catalog rows.A format-agnostic origin/remote pair behind seven
Adapterseams. Four independent guards over the mutable state, with no lock held across an adapter call, an origin fill, or aResponseWriterwrite; the Acceptance's concurrent two-read test is what that exists for, and it runs under-race. -
Step 5a: the shared seed-and-settle helper.
pkg/conformance/seed.go. Covers AC #14 (closed), #15 (closed), #24 (closed), #33, #37 and §Preflight's cause table. No catalog rows.The helper classifies from no format literal:
Storedis the callback's verdict, so there is no2xxcheck and no202case. The resume trigger is one sticky condition rather than a list of the four shapes that reach it, which is what makesPreexistingon the first attempt content-checked with no re-attempt.
Three defects the test-first cycle caught, worth a reviewer's eye
Each is its own commit, so the sequence stays legible.
-
bb098eb: the expiry message fired on any cancellation, not just a deadline. Step 4b implemented the condition the plan pinned,ctx.Err() != nil, whichcontext.Canceledalso satisfies. An operator pressing Ctrl-C on a remote run withnabove zero was toldrun cancelled after 8m0s: 3 rows seed against --upstream-url ... so this run needs --timeout >= 6m0s: advice about a deadline that never fired.AC #31 (closed) and §Seeding are both worded over expiry, and §Error Cases calls its row the root-context-expiry row, so the plan's mechanic was broader than the spec it implements.
errors.Is(ctx.Err(), context.DeadlineExceeded)satisfies both concerns: reading the CLI's own context rather thanrunErris still load-bearing, because the library client's per-request timeout also wrapsDeadlineExceededand would misreport a stalled seeding write. Neither half alone is enough, and the test pins the pair. -
4ef7056and669ea07: two assertions that could never pass, and the linter loop behind them. Both compared the media typeapplication/jsonwithrequire.JSONEq, which unmarshals its expected argument and so rejected it as invalid JSON before comparing anything.require.Equalis what they meant, but that re-triggerstestifylint'sencoded-comparerule, which is what pushed the original code toJSONEq. Renaming the constant did not clear it, so the heuristic keys on the value; anolintwith the reasoning is the honest resolution, and the repo already uses that pattern at 224 sites. -
259648d: an assertion no correct implementation could satisfy. One preflight-message row expectedauthorization: Bearer [REDACTED], butScanContent's value class is[^\r\n]*from the separator run, soBeareris consumed.redact_test.go:165pins exactly that. Decisively, the same test file already contradicted itself:assertBothScrubPassesasserts the output contains"authorization: " + Sentinelover the same fixture. And AC #24 (closed) pins the composition asRedactURLUserinfo(ScanContent(...)), so producingBearer [REDACTED]would require not callingScanContent.
Plan corrections and open items
docs/plans/2026-08-21-remote-conformance.md gains three corrections and one
new spec-amendment candidate, all verified against S08 before filing:
- Step 4b's Acceptance said no derived-
--timeoutrendering may name a bound the expired value is already above. Literally false: a derived--timeoutis2n × d + 5m, above the2n × dthe base fragment names. §Seeding says the at-or-above-floor branch "appends" and that "Neither arm names the floor", where the arms are the two moves. Scoped to the move. - Step 4b's Acceptance claimed the absent-
--repository-kindrun is the only invocation catching a!= RepositoryKindHostedbranch. Registering the flag withValue: hostedper S04's Default column (this MR's parent, !258 (merged)) makes the flag readhostedwhether or not it is on argv, so the discriminator is the programmatic path. Both rows are asserted. - Step 5a's Acceptance said the upstream-failure-status read is "asserted here".
Not implementable: that read goes through
env.Client()against the repository under test, andSeedAndSettleonly reads the upstream. Reassigned to Steps 9, 19 and 30. - New candidate: S08 §Seeding pins the base expiry fragment and the
at-or-above-floor branch's appended fragment, but not the separator between
them. One byte the spec leaves free in an operator-facing contract string.
"; "chosen and asserted exactly, so a divergence fails loudly.
Three design gaps are recorded rather than worked around, each parked at the step that will have the evidence to decide it:
remotefakehas no absence-body seam. AC #17 (closed) requires the not-found status and error shape;RefusedBodyis refusal-scoped. Surfaces at Steps 13, 23 and 35.- The
remotefakeorigin models no OCI upload session (POST blobs/uploads/→PATCH→PUT ?digest=), storing atCoord.Keyon any write verb. Surfaces at Step 30, whose callback readsBlobUploadResult.Method. SeedAndSettle's two returns cannot signal a cancellation. With no*SeedErrorproduced, a cancelled call returns nil alongside whateverSeedResultthe last write or read-back observed, which is typically non-zero and is indistinguishable from success unless the caller re-checksctx.Err(). It is not a zeroSeedResult, sores == SeedResult{}is not a discriminator in either direction. The signature is deliberately not widened here: three bands code against it in parallel and Band A is what they wait on.SeedAndSettle's godoc states the re-check as a caller requirement so the rule reaches a band author readinggo doc; the first preflight caller decides whether to widen.
.gitignore gains **/testdata/rapid/. pgregory.net/rapid writes a repro file
per property failure, and during a test-first step the properties fail by design
and regenerate on every run, so nothing should be able to smuggle them into a
commit.
Test plan
-
go test ./...green. -
go test -race ./pkg/conformance/...green, no data races. -
golangci-lint run ./...(2.13.1, the pinned version): 0 issues repo-wide. -
gofmtandgoimportsclean on every changed file. - No catalog rows in this batch, so no catalog Status flips and no reference-validation layer applies. Cluster 1 carries 0 rows per annex §7.
Three files flag under a bare gofmt -l (pkg/conformance/maven/module.go,
maven/publish_snapshot.go, npm/module.go). Pre-existing drift on main, no
pre-commit hook flags it, untouched here.
Process
Each step followed the repo's test-first authorship contract: a test(...)
commit establishing the failing floor, then the implementation. The commits are
separately visible and the branch is not squashed. The --no-verify exception is
used once per step, on the test-author commit only; on a batched branch that is
three commits here, which is expected rather than a defect and is reasoned out in
validation/decisions.md D7.
Deferred findings are logged in validation/decisions.md (D1-D11) rather than
fixed silently or dropped, and departures from the plan are in
validation/deviations.md. This batch came from a goal run whose annex §7
defines its scope; that run also produced a reference environment and eight
measured findings about the real Artifact Registry, which shaped two things here
even though this batch has no catalog rows: remotefake's StateHeaders seam
had to stay expressive enough for four different cold-to-warm transitions
rather than one, and Step 5a's tests had to avoid assuming any second write is
refused, because a retried Maven seed writes byte-identical content and returns
200.
Review round
Five commits on top of the batch, from a full-branch review. Each carries its
own reasoning; the headline is that NewSeedFailureCase wrote an untruncated
response body into HTTPDetail.ResponseBody, which S04 §Report schema bounds
at 4 KiB and explicitly does not let the renderer re-truncate. On AC #37's
mismatched settle that body is a successful read-back's served artifact, up
to 32 MiB for an OCI manifest, and it reached TestCase.Message and the JUnit
<skipped message> attribute at full size.
11a4ecdthe 4 KiB bound, the one-line skip reason S04 AC #65 pins, and a duplicate-refusal verdict the cause sort could not see when the callback reported it without an error.011ae64the expiry wrap was shadowing*FilterNoMatchErrorand*PriorityNoMatchError, printing both through theirError()renderings rather than the ones S04 AC #70 and #71 pin.ea24a75four plan claims the Band A edits left stale, including Step 4b still prescribing in bold thectx.Err() != nilmechanicbb098ebremoved.aeef5edself-review: a nil guard onNewSeedFailureCaseand two doc claims narrowed to what the code does.8de8061merge with !258 (merged), whose own review round movedConfig.Validateahead of the credential read. The two reorders ofrunActionare reconciled with validation first, andTestRunCommand_ValidationPrecedesTheSizingpins that order, which nothing did before.
Each fix was mutation-checked: reverting it alone fails the suite.