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
Settingsstub becomes a closed responseoneOf: three branches (docker and oci share the container branch, maven and npm have their own),additionalProperties: false, credential objectswriteOnlyand 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 inlist.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 byTestListHandler_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