feat(remote): Probe operation over the upstream doer seam (S13 step 9)

📦 What this MR does

S13 Step 9: Prober, the remote repository's Probe operation — the parallel existence check the virtual resolver fans out across remote upstreams to decide which of them holds a file without transferring bodies.

  • A HEAD through the slice-provided RequestBuilder and the security-hardened client behind the UpstreamDoer seam (!1109 (merged)), under the fail-small BodyKindMetadata cap, carrying If-None-Match exactly when the caller supplies a stored ETag.
  • Taxonomy mapping per the spec: 200ProbeHit, conditional 304ProbeFresh, 404ProbeMiss, any other status → ProbeError carrying that status. A transport failure — blocked URL, unsafe redirect, and probe deadline included — is positional data for the resolver (ProbeError, zero status, nil error); only context.Canceled propagates as a Go error, so the resolver can tell caller abandonment apart from upstream failure.
  • Unsolicited 304 maps to ProbeHit, not ProbeFresh (ratified during implementation): a 304 answering a HEAD that sent no If-None-Match is a protocol violation but still an existence claim; mapping it to ProbeFresh would make the resolver serve a cached copy that a fresh-miss position lacks and a stale no-ETag row must not serve on age alone. FetchFreshFromCache gets the mirror note so the Fetch stack inherits the decision at the taxonomy layer; the spec's unqualified 304 → ProbeFresh wording is queued for the spec-amendment work item (see follow-ups).
  • Seam-contract hardening from the review rounds: RequestBuilder.BuildRequest must bind the returned request to ctx; UpstreamDoer.Do must return a non-nil Body on a nil error and keep context errors errors.Is-visible — each stated on the interface so the step 11 production adapter has a normative contract to conform to.
  • remotetest upgrades the probe tests need and later steps reuse: FakeUpstreamDoer normalizes a scripted nil Body to http.NoBody on a shallow copy (mirroring the established nil-Header normalization), plus a shared mutex-guarded CloseRecorder.
  • Step 9 plan edits under the documented drift carve-out: Depends on: gains step 7 part 1 (the UpstreamDoer seam Probe consumes — internal/remote cannot import upstreamhttp back without a cycle), and the Tests: line is corrected from httptest to the canonical remotetest fakes (no concrete adapter exists until step 11).
  • Plan: docs/plans/2026-07-16-s13-virtual-remote-foundation.md — Step 9
  • Spec: docs/specs/S13-virtual-remote-foundation.md — Probe, virtual resolution Phase 2, security covers

🎯 Spec coverage

Acceptance / contract point Test
200/304/404/other status taxonomy TestProber_Probe
Conditional HEAD with matching ETag → ProbeFresh TestProber_Probe + TestProber_Probe_RequestDiscipline
Unsolicited 304ProbeHit TestProber_Probe
Blocked URL / unsafe redirect / header injection / deadline → positional failure, nil error TestProber_Probe_TransportFailureIsPositional
HEAD-only, BodyKindMetadata, no cap override, conditional-header discipline TestProber_Probe_RequestDiscipline
context.Canceled propagates as the only doer-side error TestProber_Probe_CanceledPropagates
Request-build failure propagates, no upstream call TestProber_Probe_BuildErrorPropagates
Response body closed TestProber_Probe_ClosesResponseBody
Nil-seam guard → ErrNotImplemented TestProber_NilSeams
Fake nil-Body normalization on a shallow copy TestFakeUpstreamDoer_NormalizesNilBody

🔬 e2e scenarios

No scenario added or affected: Probe is format-agnostic service-layer plumbing with no reachable request path until step 11 wires the production adapter and step 12 composes it into virtual resolution.

🚧 Follow-ups

  • Consolidate the two test-local close-recording bodies in internal/remote/upstreamhttp onto remotetest.CloseRecorder (touching those files is outside this step's scope).
  • Spec amendment: qualify the 304 → ProbeFresh rows with the conditional-probe precondition and record the unsolicited-304 → ProbeHit rule — folds into #320 (closed).

Related to #330 (closed)

Merge request reports

Loading
Loading