feat(managementapi): serialize remote settings on detail and list

Stacked on !1572 (merged).

Why

Remote repositories keep their upstream configuration (URL, cache windows, credential state, health) in per-format child tables, but the management API's read surface serializes none of it: Repository.Settings is an empty stub, so an operator listing or inspecting remote repositories cannot see where a remote points or whether it is healthy. This MR lands the read side. Detail and list responses serialize settings for remote rows through the merged decode/projection plumbing, and the response contract closes per format family.

Step 12 of the merged plan 2026-08-13-s17-phase6-virtual-remote-repositories, implementing the response side of S17 AC #68 plus AC #74.

What (the non-obvious parts)

  • The list resolves settings in one batched child-table read per format family present in the page, after the untouched keyset scan, never per row. Integration tests pin the SQL statement count, and a full-page case (100 container rows) pins one batch query.
  • A details miss on the detail route re-reads the parent by name: a vanished or replaced parent gets the existence-hiding 404, a still-live parent stays 500, and a failed re-read falls back to 500. The list keeps the whole-page 500 on that race, documented on errRemoteDetailsMissing.
  • The OpenAPI Settings stub becomes a closed response oneOf: three branches (docker and oci share the container branch, maven and npm have their own), additionalProperties: false, credential objects writeOnly and never required. Request schemas stay untouched. The permissive request union joins in Steps 23, 24, and 34.

Batch contracts

Promised during the review of feat(managementapi): serialize remote settings ... (!1572 - merged) • Hayley Swimelar • 19.3: this MR settles the three cross-family batch contracts, which the store seam fixes per format.

  • Cardinality: the container and maven batches reject a set above their 100-id caps, and npm is deliberately uncapped, with the caller-bounds rationale in its batch doc comment. maxPageSize (100) carries the reciprocal move-together comment, a value-pin test (TestMaxPageSize_MatchesRemoteDetailsBatchCaps), and the full-page one-batch integration tie. Known residual: a lowered maven cap has no executable tie, documented in list.go.
  • uuid.Nil: the container and maven batches reject a nil member, and npm omits it like any unknown id, a divergence its batch doc comment names. Every singular read rejects nil ids.
  • Maven's pointer-valued batch map: normalized at the managementapi seam (mavenBatchDetails), so a present-but-nil entry resolves to the missing-row 500, pinned by TestListHandler_MavenBatchNilEntry_Returns500.

Constants

The containerRemoteDetailsBatchLimit-vs-maxPageSize duplication was routed here by a Duo decline on chore(datastore): add container remote reposito... (!1578 - merged) • Hayley Swimelar • 19.3. Resolution: both constants stay unexported and value-matched. datastore cannot import managementapi, so the tie is reciprocal doc comments at both sites plus the executable pins above. No export and no shared package: the two values are separate knobs whose equality is a contract, not one constant.

Size

3,507 reviewable LOC: tests ~2,495, production Go ~784 (managementapi serialization plus wire adapters), OpenAPI 228. This is one plan step, and the bulk is the pinning test surface (unit tables over fake readers, a contract sweep, an integration suite with SQL statement counting). Splitting would sever the pins from the serialization they define.

Test plan

  • Unit: handler tables over fake details readers. Positive settings hits for docker, oci, maven, and npm rows, omission for hosted and virtual rows, the 500 paths, and redaction probes.
  • Contract: response validation against the closed oneOf, plus a handler-to-contract sweep.
  • Integration (testcontainers PG): detail and mixed-kind list pages with SQL statement counts, the 100-row full-page one-batch tie, and raw-body secret probes over seeded plaintext columns.
  • No docs/testing/ e2e scenario added: the catalogs cover protocol-client journeys, and this is read-side management-API serialization, covered by the settings read integration suite.

Spec coverage

Spec: docs/specs/S17-rest-management-api.md

List-and-detail settings slice only (plan Step 12): the settings write surfaces (create echo, PATCH merge, request-side union) belong to the remote create, PATCH, and URL-change steps and their MRs. Criteria not listed here belong to other steps of the merged plan.

Acceptance criteria

# Criterion (this step's slice) Tests
AC-7 Detail emits no settings key for a hosted repository (the Phase 1 form, narrowed to hosted rows by Phase 6) Pre-existing, unchanged: TestDetailHandler_ExistingRepository_Returns200; kind-blind-implementation traps: TestDetailHandler_NonRemoteRows_OmitSettings, TestListHandler_PageWithoutRemoteRows_NoDetailsReads
AC-68 Response side: settings is a oneOf of closed per-family branches, each requiring its format's fields, credential objects write-only; validates in CI alongside the earlier surfaces TestContract_SettingsResponse_OneOfClosedBranches, TestContract_SettingsResponse_DiscriminatesFamilyPayloads; handler tie: TestRemoteSettingsResponses_MatchOpenAPIContract. Request-side permissive union: Steps 23/24/34
AC-71 A virtual repository's responses carry no settings key (read side of the settings-less virtual shape) TestDetailHandler_NonRemoteRows_OmitSettings, TestListHandler_MixedKindPage_SerializesRemoteSettings, TestRemoteSettingsIntegration_ListMixedKindPage
AC-74 Detail and list serialize settings for remote rows (url, cache windows, has_credentials, health fields); cleartext credentials in no response; the list's fetch is page-bounded, one child-table lookup set per family present in the page - per-row fetching fails the criterion TestDetailHandler_RemoteRepository_SerializesSettings (docker, oci, maven, npm each a positive hit), TestListHandler_MixedKindPage_SerializesRemoteSettings, TestListHandler_SingleFamilyPage_OneBatchRead, TestRemoteSettingsIntegration_Detail, TestRemoteSettingsIntegration_ListMixedKindPage (SQL statement count per family), TestRemoteSettingsIntegration_FullContainerPage_OneBatchQuery (the 100-row executable tie between maxPageSize and the details batch cap), TestMaxPageSize_MatchesRemoteDetailsBatchCaps (value pin), TestListHandler_MavenBatchNilEntry_Returns500 (pointer-map normalization)

Error cases

# Condition (this step's slice) Tests
E-1 Detail: repository missing (404, existence hiding) Pre-existing, unchanged: TestDetailHandler_MissingRepository_Returns404, TestDetailHandler_CrossSlug_Returns404; the details-miss liveness re-read: TestDetailHandler_RemoteDetailsMissVanishedRow_Returns404, TestDetailHandler_RemoteDetailsMissRecreatedRow_Returns404
E-2 All: unexpected server failure (500) - a failing or inconsistent details read on either route TestDetailHandler_RemoteDetailsFailure_Returns500, TestDetailHandler_RemoteDetailsMissing_Returns500, TestDetailHandler_RemoteDetailsNilRow_Returns500, TestDetailHandler_RemoteDetailsInvalidHealthStatus_Returns500, TestListHandler_RemoteDetailsBatchFailure_Returns500, TestListHandler_RemoteRowMissingFromBatch_Returns500, TestDetailHandler_RemoteDetailsMissRereadFailure_Returns500, TestDetailHandler_RemoteDetailsFailure_SkipsLivenessReread
E-3 List: invalid format/kind/sort/order/limit/cursor (400) Pre-existing, unchanged: TestListHandler_InvalidParams_Return400 (list_test.go); settings resolution adds no new parameter

Security considerations

# Concern Tests
S-1 Upstream credentials: write-only, surfaced only as has_credentials, absent from responses and error bodies Exact-map settings equality on every unit case (no credential key can appear); raw-body probes over seeded plaintext columns and the container auth memo: assertNoSeededSecret in TestRemoteSettingsIntegration_Detail and TestRemoteSettingsIntegration_ListMixedKindPage; contract side: write-only marking pinned by TestContract_SettingsResponse_OneOfClosedBranches and the response-validation rejects in TestContract_SettingsResponse_DiscriminatesFamilyPayloads. The details projections carry no credential field by construction (datastore suites own that pin)
S-2 Tenant isolation: every details read scoped by the resolved namespace id Seam-level: namespace-id assertions on every recorded find and batch call (TestDetailHandler_RemoteRepository_SerializesSettings, TestListHandler_MixedKindPage_SerializesRemoteSettings); store-side scoping owned by the datastore details suites
S-3 Injection: jet builders with bound parameters No new SQL: the handlers compose the existing details reads, whose statements the datastore suites own
S-4 Error bodies never echo driver error text TestDetailHandler_RemoteDetailsFailure_Returns500, TestListHandler_RemoteDetailsBatchFailure_Returns500 (leak probes on the boom text)

Related to #314

Merge request reports

Loading
Loading