test(oci): OCI strict-equality flip, Step 37 (S08 remote, batch 12/12)

Summary

OCI strict-equality flip per the S08 remote goal-run's annex §7 MR clusters ("OCI strict-equality flip"). Lands Step 37: no new catalog rows, and the OCI band's inventory guard turns from a subset relation into strict equality.

Related to #47 (closed). Divergence index: #54.

Stacked on !272 (merged) (sshen/oci-remote-discovery-batch, Steps 34-36), which is stacked on !271 (merged), which is stacked on !266 (merged). The flip asserts that every non-optional oci.remote.* catalog row has a descriptor and vice versa, so it is only true once every row in the band is registered. Do not merge before any of the three.

What it lands

Strict equality in both directions, under every kind. The per-kind switch is deleted rather than set to true three times, so no later step can turn the Catalog ⊆ Module direction off for one kind and leave the other two looking guarded. oci.remote.cache-ttl and oci.remote.cache-invalidation are excluded because both are already optional on main.

AC #35's four pairing guards. oci.remote.not-found and oci.remote.write-refused-blob-upload have NeedsUpstream false, every other non-optional ## Remote row has it true, the two sets partition that section, and the module registers at least one oci.remote. descriptor with NeedsUpstream: true — stated over the module's descriptors and the slug prefix rather than over section membership, as the plan requires.

The SetupFailure propagation guard (remote_band_test.go, new). Every non-optional oci.remote.* descriptor carrying NeedsUpstream is driven once with an injected run-attributable seeding fault and asserted to return a case with a non-nil SetupFailure.

Why the propagation guard is the point of this MR

A row that hand-builds its TestCase instead of calling NewSeedFailureCase demotes a revoked credential to a plain skip. AC #33 cannot see it because the field is unset, and AC #36 (closed) cannot see it because not every seeding row skipped. The run exits 0 having validated nothing.

Two mutations show the gap is real rather than theoretical. A seeding row registered in module.go, with a matching catalog row, absent from every per-file row list, that either hand-builds its skip or fabricates a *SeedError without contacting the upstream: every inventory relation stays green, and all six per-file RouteASeedingFailureThroughNewSeedFailureCase tests stay green. Only the new guard reports it.

It lands here rather than earlier for the same reason the pairing assertion does: it is only complete once every row in the band is registered.

A green here is evidence about remotefake, not about a provisioned repository

Stated in the guard's own doc rather than only here. S08 §Dependencies names the real-target run that gates enabling this format's remote CI gate; this MR does not discharge it.

Two things a reviewer should look at

Mutation 12 proves a claim that would otherwise be vacuous. Strict equality between two empty sets is trivially true, so emptying remoteCatalog() and deleting every oci.remote. catalog row leaves the equality green. What fails is TestInventory_moduleRegistersARemoteUpstreamRow and TestInventory_remotePrefixMatchesCatalog. Without those two the flip could be satisfied by having no rows at all.

Two files outside Step 37's list were touched, both forced. The propagation guard needs remoteSeedingFaults / requireSeedingFailureCase / newRemotePair, all unexported in package oci, while inventory_test.go is package oci_test — reuse and single-file scope are mutually exclusive, so the guard is a new package oci file and both headers say why. And docs/catalog/oci.md's ## Remote preamble said "the rest of S08's inventory lands with the per-format children", which the S08 row this MR edits now contradicts; verified first that OCI's catalog does carry the full inventory (S08 names 15 oci.remote. slugs and the catalog has exactly those 15 rows).

The Owner cells carry absolute numbers at HEAD

Nothing is left for a post-merge relabel, and the earlier version of this section asking for one is withdrawn. docs/dev/conventions.md §Amendment tables bans relative forms and asks for the number to be filled in once the MR is open, which HEAD does: S08's ## Remote rows row names !261, !263, !282, !266, !271 and !272, with !273 appended for the preamble clause this MR ships, and the flip row below it names !273.

The prose collision with !263 (merged) that this section used to flag is also gone. That sentence reads "Maven's, OCI's, and npm's catalogs each carry their full remote inventory" at HEAD, so there is no per-format narrowing left for either MR to resolve to "Maven only".

Pre-existing staleness not fixed here

The same ## Remote preamble still says internal/cli implements --registry-url and neither --upstream-url nor --repository-kind. That is false as of !258 (merged), which is on main. It predates this branch and Step 37 does not make it worse. !263 (merged) left npm's identical preamble stale too, so it is a three-format cleanup rather than a fix belonging to this MR.

Spec coverage

Spec: docs/specs/S08-remote-contracts.md

Rows whose behavior this MR does not own say where the coverage lives instead, per docs/dev/go-testing.md §Spec-coverage table. The 14-row mutation table in the commit body answers a different question (which test kills which mutation) and is not a substitute for this one.

Acceptance criteria

# Criterion Tests
AC #27 (closed) Every descriptor registered for a kind maps to a non-optional row in that kind's bucket or the kind-independent bucket TestInventory_moduleSlugsMatchCatalogPerKind, now over all three kinds in both directions
AC #27 (closed) The guard rejects a descriptor with no row TestInventory_remoteGuardRejectsAnUnregisteredRemoteSlug; per-kind keep-list in TestInventory_registeredSlugsRespectThePerKindKeepList. Both pre-existing, untouched here
AC #27 (closed) "The guard is a subset relation; the flip to strict equality stays a per-format plan concern" No test asserts subset-ness. This MR asserts strict equality, which the AC sanctions as the per-format plan's call (plan §Step 37)
AC #33 A run-attributable seeding refusal reaches the runner as TestCase.SetupFailure, from every band row rather than per file TestRemoteBand_EverySeedingRowPropagatesSetupFailure
AC #33 Exit 2 carrying the failure's detail with no further row, the first-attempt versus resuming-second-attempt split, and AC #37's duplicate-refusal exception Runner-owned, outside this MR: pkg/conformance/run_remote_seeding_test.go, pkg/conformance/run_remote_verdict_test.go; the per-cause sort is TestTestRemotePreflight_SortsASeedingFailureByItsCause
AC #35 (1) The two named upstream-free rows have NeedsUpstream false TestInventory_remoteUpstreamFreeRowsNeedNoUpstream
AC #35 (2) Every other non-optional ## Remote row has it true TestInventory_remoteSeedingRowsNeedUpstream
AC #35 (3) The two sets together are exactly that section's non-optional rows TestInventory_remoteBandPartitionsByNeedsUpstream
AC #35 (4) The module registers at least one oci.remote. descriptor with NeedsUpstream: true TestInventory_moduleRegistersARemoteUpstreamRow
AC #35 The oci.remote. selector all four sets are read through is neither empty nor widened TestRemoteBand_ThePrefixSelectsExactlyTheRegisteredBand on the module side, TestInventory_remotePrefixMatchesCatalog on the catalog side
AC #35 Class derivation (Relay / Repeat read / Seeded write refusal true, Absence / Write refusal false) Deliberately unasserted: the catalog has no class column, so AC #35's own closing paragraph names the exceptions at the guard instead
AC #36 (closed) The no-NeedsUpstream-descriptor branch stays unreachable for OCI, which AC #36 (closed) calls the CI-first guard for TestInventory_moduleRegistersARemoteUpstreamRow
AC #36 (closed) SetupFailure is set only through NewSeedFailureCase, on every band row TestRemoteBand_EverySeedingRowPropagatesSetupFailure
AC #36 (closed) Exit 2 after the last row, ErrNothingEstablished wrapped with its branch, Report.Interrupted, the narrowing exemption, precedence over a row's StatusFail Runner-owned, outside this MR: pkg/conformance/run_remote_verdict_test.go, pkg/conformance/run_seeding_counts_test.go, internal/cli/run_test.go

Error cases

Condition Tests
Seeding write refused 401 or 403: run-attributable, SetupFailure carries the refused write's own response TestRemoteBand_EverySeedingRowPropagatesSetupFailure band-wide, through requireSeedingFailureCase
Seeding write refused with any other status: row-attributable StatusSkip, no SetupFailure TestTestRemotePreflight_SortsASeedingFailureByItsCause and the per-file row tests, all pre-existing. The band guard reads the run-attributable cause only, since that is the one classification NewSeedFailureCase sets the field for (AC #15 (closed))
Transient causes (408, 429, 5xx, 3xx on the write, transport error, not-means-stored 2xx, unsatisfied settle): one resuming retry, then exit 2 Outside this MR. TestTestRemotePreflight_SortsASeedingFailureByItsCause holds the attempt counts
Remote kind with an upstream exposed and no seeding row completed: exit 2 with ErrNothingEstablished Runner-owned, outside this MR. TestInventory_moduleRegistersARemoteUpstreamRow is what keeps OCI out of the no-descriptor branch
--upstream-free-only run exits 0 because every seeding row skips by design Outside this MR. TestInventory_remoteUpstreamFreeRowsNeedNoUpstream pins which two rows the §Upstream gate must still run

Security considerations

Concern Tests
The SetupFailure detail and preflight Message reach stderr, the Interrupted: line and JUnit without passing the failure_detail channel, so the seeding helper scrubs at construction Unchanged by this MR, which adds no rendering path: TestSeedAndSettle_ScrubsTheDetailWithBothPasses and TestSeedAndSettle_TheScrubWindowCutsOnARuneBoundary in pkg/conformance/seed_test.go
The credential reaching both base URLs, --upstream-url inside the SSRF perimeter, redirect policy, and upstream cleanup Not in scope: this MR registers no row and issues no request outside remotefake, the in-process double

Test plan

  • go test ./... green.
  • pre-commit run --all-files clean, all 20 hooks.
  • 14 mutations, all killed; table in the commit body.

Two transient failures appeared once each during the sweep and did not reproduce: TestTestManifestSizeLimit_* (survived -count=20) and TestWaitForPackumentVersion_capElapsesAlways404 in pkg/conformance/npm, which this MR does not touch. Both look like wall-clock flakes under parallel httptest load and are recorded rather than hidden.

Process

From a goal run whose annex §7 defines this batch's scope. This step has no implementation phase: it tightens an existing guard and adds assertions, so it is one commit with no panic skeleton and no --no-verify.

Deferred findings are logged in validation/decisions.md and the plan's §Spec-amendment candidates rather than fixed silently or dropped.

Edited by Sylvia Shen

Merge request reports

Loading
Loading