feat(maven): the preflight walking skeleton, Step 9 (S08 Maven, batch 2/4)
Summary
Related to #47 (closed). Divergence index: #54.
The Maven band's walking skeleton, per the S08 Maven goal-run's annex §7 MR
clusters ("maven.remote.preflight, the walking skeleton"). Lands Step 9
and its one catalog row.
Sits directly on main. !280 (merged) (sshen/maven-remote-client-gaps, Steps 6-8)
merged on 2026-09-08 as f31bc83, so this branch no longer carries its commits.
Step 9 still depends on all three of its steps (Step 6's GetArtifactCapped and
ArtifactResponse.Method, Step 7's Upstream(), Step 8's remote catalog
bucket).
Why this step ships alone
The sibling band's annex clusters the preflight together with the three client-gap steps. This band splits at 8/9 deliberately, and the reason survives even though the original one did not: annex §7 argued the split lets cluster 1 open a review cycle earlier, which stopped applying once both Band A MRs merged. The second reason still holds. This row is the walking skeleton every later row in the band rests on, and it is worth reviewing as that rather than as the tail of a four-step diff.
Why the diff is 4,770 LOC
Above the 500 ceiling deliberately, justified here rather than split. The step
bundles the row with remotefake's Maven adapter because Step 9 §Scope
says it does, and §The shape every format band repeats makes
that the template for all three bands. The two sibling walking skeletons shipped
the same bundle and larger: npm Steps 16-19 at 11,170 reviewable LOC across 51
files (70bd7fa), OCI Steps 26-30 at 7,466 across 27 (898fce1).
| Kind | LOC |
|---|---|
| Production, the row and its callbacks | 645 |
Production, remotefake/maven.go |
842 |
| Test | 3,227 |
| Docs | 56 |
Read remote_seed.go and remote_preflight.go first: they are the 645 LOC the
row's behavior lives in. Splitting the adapter out would leave 2,775 and 1,995,
neither under the ceiling, and would land the adapter with no row exercising it.
Reference validation
Run against a live Artifact Registry with a hosted upstream and a remote repository pointed at it.
Baseline: maven.remote.preflight passes, exit 0.
Both axes, per the goal-run's requirement that a mutating operation's end
state is verified through a channel that is not the surface under test. A green
row proves the suite's assertions held; it does not prove the bytes reached the
origin, because every read the row makes is a read the row chose. Reading the
hosted upstream's own client endpoint directly gives origin 200, remote
200, bodies byte-identical at 404 bytes, and two distinct X-Request-Id
values, which is what proves two requests were served rather than one response
read twice.
Seven negative controls, seven distinct causes, each identified by its message rather than by its verdict:
| Control | Verdict | The reason the message gives |
|---|---|---|
read 503 |
FAIL | remote did not serve the seeded fixture, status 503 |
write 403 |
SKIP, exit 2 | a credential the upstream does not accept ends the run |
write 503 twice |
SKIP, exit 2 | no confirmed store on either attempt, last status 503 |
write 409 |
SKIP, exit 2 | a refusal at this row's own coordinate, says nothing about other rows |
| relay corruption | FAIL | served bytes are not this run's fixture |
| resumed settle, foreign bytes | SKIP, exit 2 | a collision: a foreign writer, or a changed fixture builder |
settle 404 |
SKIP, exit 2 | did not become readable within the settle timeout |
Getting that table honest took four corrections, and every wrong version
passed. A misspelled proxy flag meant all seven cases failed with
connection refused while showing verdicts that read exactly like working
controls. Two more came from the mutation not reaching the code it aimed at:
the proxy rewrites the response, so a refused write still lands and the
re-attempt succeeds, and the re-attempt's existence probe is a HEAD, so a
PUT-scoped mutation never sees it. The fourth was aiming at the wrong path
entirely: corrupting the settle read-back of a fresh write does not reach
AC #37 and must not, because on the fresh path the settle confirms readability
and the byte comparison belongs to the resumed path.
That is why the matrix prints each case's message. A verdict-only table would have recorded all four wrong versions as evidence.
Three things a reviewer should look at rather than skim
A test that could not fail, found by the implementation's mutation pass.
TestMavenOriginWrites_LeaveReadsAlone asserts the origin-fault hook does not
fault reads. It cannot fail: the core never consults the hook on a read, so the
test asserts a property the core guarantees rather than one the hook
does, and deleting the hook's own guard left the suite green. Closed by an
assertion that drives the installed hook directly. Both tests are kept and each
one's doc names the distinction, so the pair does not read as a duplicate the
next reader deletes.
A guard nothing could reach, and why it stayed. The hook's own
isReadMethod check is unreachable today. It stays because
StageOriginWrites' doc says the hook faults reads never, and a claim made in
a doc comment is backed at the boundary that makes it rather than inherited
from a caller.
A seam gap found one step before the row that needs it. TransformBody
carries no CacheState, so no adapter can make the double serve different
bytes on the second read of one coordinate. Step 12's repeat-read negative
direction is therefore not stageable through remotefake, and without it that
row would assert something no available mutation can break. Recorded with its
resolution in validation/decisions.md D19 before Step 12 was dispatched.
Front-loading the adapter into this step is what surfaced it that early.
Two model divergences, recorded rather than discovered later
Both are deliberate and neither failed a test, which is exactly why they are written down.
The double's Etag and X-Checksum-* values are coordinate-derived, not
content-derived, because StateHeaders is handed no body. Every assertion
over them checks the grammar and never a value, so the day OCI's
docker-content-digest row forces a content-true implementation it lands
without touching a Maven test.
The double answers 409 on any occupied coordinate, modelling a
maven_duplicates_allowed=false endpoint. The validation target runs the
default, where a second write of identical bytes is 200 and silently
idempotent. Under that default AC #37's resume path is unreachable, since the
fixture is a pure function of (run ID, slug) and a re-attempt writes identical
content. So a green AC #37 test against the double is evidence about the
duplicates-disallowed configuration only, and the reference-side validation
reaches the same code path through the Preexisting trigger instead. That is
why the row asserts both triggers.
Spec coverage
Every Acceptance item Step 9 owns, and the tests that pin it. Squash is on, so
this table lives here rather than only in cadc704's commit body.
| Spec item | Behavior | Tests |
|---|---|---|
| S08 AC #13 (closed) (byte half) | A coordinate seeded through --upstream-url reads back byte-identical through --registry-url | TestRemotePreflight_PassesAgainstARemotefakePair, TestRemotePreflight_ReadBackFailureCarriesThePinnedMessage |
| S08 AC #13 (closed) (metadata half) | The format's integrity metadata agrees with the bytes | Not this row. Lands with maven.remote.sidecar-synthesis at Step 11 (AC #20 (closed)); classification tension recorded under the plan's §Spec-amendment candidates, Band B 2 |
| S08 AC #14 (closed) | SettleTimeout zero is exactly one read-back, no polling | TestRemoteSeedSpec_FillsEveryMemberTheHelperReads; loop owned by Step 5a's seed_test.go |
| S08 AC #15 (closed) | A refusal at the row's own coordinate skips the row alone, reason naming --upstream-url and the status | TestRemotePreflight_SeedingFailuresSortByCause, TestRemoteSeedWrite_CarriesTheRefusalResponseForTheSkipDetail |
| S08 AC #24 (closed) | Ordered first among a remote run's descriptors; message renders both base URLs through redact.RedactURLUserinfo and the observed body through the scan-then-userinfo composition | TestRemotePreflight_IsFirstInTheWholeRemoteCatalog, TestRemotePreflight_ReadBackFailureCarriesThePinnedMessage, TestListCommand_MavenRemoteRowsAreExactlyTheKindIndependentBucket |
| S08 AC #29 (closed) | A cancelled run reports no compliance verdict | TestRemotePreflight_CancelledSeedSkipsRatherThanFails, TestRemotePreflight_CancelledRelayReadSkipsRatherThanFails |
| S08 AC #33 | A write that lands and never settles ends the run with exit 2 | TestRemotePreflight_SeedingFailuresSortByCause, TestRemotePreflight_UnderTheRunner |
| S08 AC #35, #36 (closed) | NeedsUpstream gates the row under --upstream-free-only and feeds the post-loop verdict | TestRemotePreflight_UnderTheRunner |
| S08 AC #37 | A resumed settle passes only on content identity | TestRemotePreflight_ResumedSettleRequiresContentIdentity, TestRemoteSeedWrite_ReportsPreexistingAlongsideAFailedLaterArtifact |
| S08 §Preflight, means-stored | Any 2xx but 202, and only when the terminal request used the write verb | TestRemoteSeedWrite_ReportsMavenStoredVerdicts, TestRemoteSeedWrite_MethodSwitchedRedirectIsNotStored, TestRemoteSeedWrite_MethodSwitchedRedirectIsNotStoredWhenTheReplayFails |
| S08 §Preflight, cause table | Each write failure sorts to the row the spec assigns | TestRemotePreflight_SeedingFailuresSortByCause; the sort itself is Step 5a's |
| S08 §Fixture seeding model | Fixture bytes are a pure function of (run ID, slug) | TestRemoteJARArtifact_IsAPureFunctionOfRunIDAndSlug, TestRemoteCoords_SpellTheRunScopedCoordinate |
| S08 §Seeding and asynchronous upstreams | Both seeding legs address --upstream-url, never the repository under test | TestRemoteSeedSpec_CallbacksAddressTheUpstream |
| S08 §Error Cases | An upstream-failure status on the assertion read is a finding, not a skip | TestRemotePreflight_UpstreamFailureStatusOnTheAssertionReadFails, TestRemotePreflight_TransportFailureOnTheRelayReadNamesTheTransport |
| S05 §Artifact coordinates | The run-scoped release coordinate and its request path | TestRemoteCoords_SpellTheRunScopedCoordinate |
| S04 §Test naming convention | Every descriptor reports its own slug | TestInventory_everyDescriptorFnReportsItsOwnSlug |
The table was missing from both test-author commit bodies and from the MR description, which docs/dev/go-testing.md §Spec-coverage table requires; it is what lets a reviewer check twelve claimed ACs without reading the spec beside 2600 lines of tests.
Raised in review on !281 (merged).
Falsification
Every facet was checked against the change it is supposed to catch, one
mutation at a time. The adapter: twelve mutations, eleven caught, and the
survivor was the uncatchable test above. The row and its callbacks: twenty
mutations, eighteen caught. Of the two survivors one was a real gap, now
closed, where nothing held remoteSeedWrite to stopping at the first
not-stored artifact because no test put a not-stored artifact in front of
another one. The other is unkillable by construction: PutArtifactRelease
returns a nil error only on a 2xx, so mavenMeansStored's upper bound cannot
be reached, and its doc says the range is written out so a reader can check it
against the spec sentence rather than because a caller reaches it.
One finding is worth repeating outside the commit messages, because it shaped
how AC #37 is staged: once an existence probe exists, the 409 resume
trigger is unreachable through plain remotefake staging. A coordinate
occupied at probe time takes the Preexisting path and never draws the 409.
The 409 arms therefore stage a lost-write-then-refusal sequence, first PUT
faulting at the transport and the second finding the coordinate seeded and
answering 409, which is the sequence AC #37 actually describes.
One deferral, recorded rather than silent
Step 9's Files prose gives remote_seed.go a metadata-PUT write verdict. It
is not here: maven.remote.preflight seeds one .jar, no Acceptance clause
touches the metadata leg, and shipping it would have meant shipping a
production path this MR does not test. Step 11 is where it becomes live,
because that step's row settles on a maven-metadata.xml it seeded, so the
leg and its test land beside the row that needs them. validation/decisions.md
D20, and it is carried in the run's progress notes as an obligation on Step 11
rather than only in a ledger, because a deferral that never reaches the step
inheriting it has been dropped rather than deferred.
Process
This batch comes from a goal run, which is why it batches by cluster rather
than opening one MR per step; the deviation is recorded in
validation/deviations.md D9. The per-step commit discipline is kept: a
test-author commit then an implementation commit for each of the step's two
slices, separately visible in the branch history, conventional commits.
Squash is on, settled in review: the eleven slices stay readable here on the
MR, and main takes one commit, which is what !280 (merged), the npm batch and both OCI
batches did. Two commits use --no-verify, both test-author, which is the
documented test-first exception and the expected count for a two-slice step.
The step was split at the package boundary after two full-step dispatches died on infrastructure, which is why the history shows an adapter pair and a row pair rather than one of each.
Judgement calls and deferred findings are in validation/decisions.md (D19,
D20) and validation/reference-quirks.md rather than fixed silently or
dropped.
Test plan
-
go build ./...andgo vet ./...clean. -
go test ./... -count=1green, no regressions in any sibling package. -
go test -racegreen on both touched packages. Not optional forremotefake: it is shared infrastructure driven concurrently by three format bands. -
golangci-lint runclean on every touched package, run over the whole package rather than the changed hunks, sincefunlen,gocognit,gocycloandduplare whole-function linters the changed-hunk hook cannot see. -
gofmt/goimportsclean on every file touched. - No new
go.mod/go.sumdependency. - Exactly one
--no-verifycommit per step slice, verified in the branch history. - Reference validation: baseline pass, both axes verified independently, seven of seven negative controls firing for distinct reasons.