feat(maven): metadata proxy miss and stale revalidation
Summary
A cache miss or stale maven-metadata.xml on a remote Maven repository used to return 501. It now fetches the upstream document, streams it unchanged, and commits the blob plus all four digest columns — the same miss/stale sequence Step 14 already shipped for artifacts.
Fresh hits stay on Step 11. This MR only owns the fill arms.
Why this shape
The metadata fill is a deliberate twin of fillRemoteArtifact. Freshness stays in Step 8's verdict; this handler does not do window math. Package-level rows keep a NULL version id. Version-level snapshot documents sit under their version row. snapshot_metadata_always_revalidate is consumed as that verdict, not as a branch in the handler.
Criterion 3's second half — a client that reads the new snapshot timestamp and then pulls the dated artifact — is a two-path sequence. It stays on Step 18's harness.
Shared-file touches on Step 14's remote_artifact.go are logging parity and //nolint:dupl for the twin, plus a redundant CodeInternalServerError case drop. remote_serve.go only updates comments that still pointed at the stub.
Size
Past the 500-LOC reviewable ceiling (development-model.md). Split would not help: one fill, one suite, one stub deletion. A tests-first stack does not compile until fillRemoteMetadata exists.
| Group | + | − |
|---|---|---|
Production Go (remote_metadata.go, stub delete, small artifact/serve twins) |
~629 | ~62 |
| Tests (integration + dispatch + serve-handoff) | ~1,771 | ~12 |
| Plan record | ~50 | ~6 |
| Total | ~2,450 | ~80 |
Most of the overage is remote_metadata_integration_test.go (~1,617). Same pattern as Step 14's artifact suite.
Spec coverage
Spec: docs/specs/S14-maven-remote.md
Acceptance criteria
| # | Criterion | Tests |
|---|---|---|
| MP-1 | Verbatim relay | TestRemoteMetadata_Miss200_FillsStreamsAndCommits |
| MP-2 post-window | After the window: 304 bumps, 200 replaces | TestRemoteMetadata_Stale304_BumpsAndServes, TestRemoteMetadata_Stale200_ReplacesRow |
| MP-2 fresh half | Second GET inside the window serves from cache | Step 11 |
| MP-3 metadata-fill half | Snapshot document anchors under its version row and refreshes under the metadata window | TestRemoteMetadata_SnapshotDefaultWindow_AnchorsUnderVersionAndRevalidates |
| MP-3 artifact follow-through | Client resolves the new timestamp/build and pulls the dated artifact | Step 18 harness |
| MP-4 | Always-revalidate opt-in; package-level still windowed | TestRemoteMetadata_SnapshotAlwaysRevalidate_RevalidatesEveryGet_304Bumps, _200Replaces, _PackageLevelUnaffected |
| MP-5 | Metadata sidecar cache-generation consistency | Step 12 / 16 |
| MP-6 | Group-level: too-short 400; two+ segments proxied as package-level | TestRemoteMetadata_GroupLevelTooShort_400BeforeLookup, TestRemoteMetadata_GroupLevelTwoPlusSegments_ProxiedAsPackageLevel |
| MP-7 | Single-flight; leader failure reaches followers | TestRemoteMetadata_ConcurrentMiss_SingleUpstreamGet, TestRemoteMetadata_ConcurrentTruncation_LeaderInterruptedFollowers503 |
| MP-8 | HEAD on a hit | Step 11 |
| MP-9 | HEAD on a miss fills | TestRemoteMetadata_HeadMiss_FillsAndReturnsHeaders |
| MP-10 | Stale hit, no stored ETag | TestRemoteMetadata_StaleNoEtag_Refetches |
| MP-11 stale half | Client conditional: upstream validator first | TestRemoteMetadata_StaleClientConditional_UpstreamValidatorFirst |
| MP-11 fresh half | If-None-Match on a fresh row | Step 11 |
| Fill-commit | Blob + four digest columns; FIPS leaves md5 NULL | TestRemoteMetadata_Miss200_FillsStreamsAndCommits, TestRemoteMetadata_Fill_MD5ColumnMatchesFIPSPosture |
Error cases
| # | Condition | Tests |
|---|---|---|
| E-404 | Upstream 404 on a miss | TestRemoteMetadata_Miss_Upstream404 |
| E-4xx | Upstream non-404 4xx → upstream_rejected |
TestRemoteMetadata_Miss_Upstream4xx_UpstreamRejected |
| E-500 | Upstream 500 → internal_server_error |
TestRemoteMetadata_Miss_Upstream5xx_Mapped |
| E-5xx | Other 5xx → upstream_unavailable; cache not served |
TestRemoteMetadata_Miss_Upstream5xx_Mapped, TestRemoteMetadata_Miss_Upstream503_RelaysUpstreamRetryAfter |
| E-transport + cache | Serve cached copy | TestRemoteMetadata_Stale_TransportFailure_ServesCached |
| E-transport, no cache | 503 + Retry-After | TestRemoteMetadata_Miss_TransportFailure_503RetryAfter |
| E-cap | Body over metadata cap treated as transport failure | TestRemoteMetadata_Miss_BodyCapBreach_Interrupted200 |
| E-group-short | Fewer than two directory segments → 400 | TestRemoteMetadata_GroupLevelTooShort_400BeforeLookup |
| Path traversal / outbound segment safety | S10 parse + Step 5 URL builder | Earlier steps |
| PUT 405 | Step 10 kind gate | Earlier steps |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S-credential hygiene | Username/password never in logs or responses | S13 client + Step 7; this fill does not log credentials |
| S-SSRF / redirect strip | S13 upstream client | S13-owned. Not tested in this MR |
| S-outbound segments | S10 grammar before URL build | Step 5 |
| S-error-payload hygiene | No upstream host/URL/headers on the client envelope | TestRemoteMetadata_Miss_Upstream4xx_UpstreamRejected (no upstream headers) |
| S-FIPS | Skip MD5 hash; leave column NULL | TestRemoteMetadata_Fill_MD5ColumnMatchesFIPSPosture |
| S-checksum trust | Sidecar attests cached bytes | Sidecar steps 12 / 16 |
E2E catalog
No Maven catalog exists under docs/testing/ yet (only README.md, docker.md, oci.md). This MR does not add one. Step 18 is the plan's hermetic Resolver sequence for this path.
Related to #286 (closed)