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 ./... and go vet ./... clean.
  • go test ./... -count=1 green, no regressions in any sibling package.
  • go test -race green on both touched packages. Not optional for remotefake: it is shared infrastructure driven concurrently by three format bands.
  • golangci-lint run clean on every touched package, run over the whole package rather than the changed hunks, since funlen, gocognit, gocyclo and dupl are whole-function linters the changed-hunk hook cannot see.
  • gofmt / goimports clean on every file touched.
  • No new go.mod / go.sum dependency.
  • Exactly one --no-verify commit 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.
Edited by Sylvia Shen

Merge request reports

Loading
Loading