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-filesclean, 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.