feat(gitlabapi): resolve a namespace by UUID
What
GET /api/gitlab/v1/namespaces/{id} (S33, Namespace resolution): the handler, the OpenAPI operation, and the wiring.
- No availability filter. A blocked, disabled, deleted, or purged namespace resolves
200with its derived status. This is the surface the platform inspects and repairs through, so applying the client-side gating predicate here would lock it out of the rows it has to fix. - Canonical ids only.
uuid.Parseaccepts the unhyphenated, brace-wrapped,urn:uuid:-prefixed, and uppercase spellings, so one namespace answered on several working URLs. The parse now round-trips throughString()and anything else is400, and the parameter declares that narrower pattern so a generated client enforces it too. The rule constrains spelling, not encoding: the segment arrives percent-decoded, so an encoded form of the canonical id still resolves. That gap is recorded where the rule is stated; closing it would mean parsingEscapedPathfor no gain against any client this surface serves. 400over404on a bad id. Reporting a typo as404would tell the platform a namespace is gone when it is not, and a platform flow may act on that destructively. The raw segment never reaches the store.- A store outage is never a
404. Onlydatastore.ErrNotFoundtakes that path; a nil row without an error is a controlled500, not a nil dereference. Both500s carry one operation-scoped log message, with the cause in the error field. - One store seam.
gitlabAPIDepstakes a singlegitlabapi.NamespaceStore, the consumer-side union of the two role interfaces, instead of the same value under one parameter per role.
Spec amendment
The spec described 404 for an unknown UUID and said nothing about a segment that is not a UUID, so this endpoint's 400 would have shipped undocumented. It also left every UUID-keyed route free to accept the alias spellings. AC 7, the resolution section, and the Error Cases table now state the canonical-form rule for the surface; conditions and verifications reuse the same guard when their steps land.
Stacking
Developed stacked on !1052 (merged) (Step 5's handler): Step 6 depends only on Step 4 in the plan, but it touches the Deps struct, the wire seam, and the provisioning test files that !1052 (merged) owned, so it branched from there to keep the diff clean. !1052 (merged) merged on 2026-07-28; this MR now targets main and is rebased onto it, so the diff is Step 6 alone.
Size
1,249 insertions and 53 deletions, of which 137 are production Go (detail.go 98, handler.go 29, wire_gitlab.go net 10). The remainder is 1,020 test lines and 92 of OpenAPI prose, spec, and CI wiring. Reviewable production surface is well inside the plan's 200-400 target. The test volume is the unit tables (lifecycle states, refusals, log guards), the canonical-spelling property, the contract sweep, and a deliberately thin integration composition: one hit and one miss through the real store. The per-column projection risk lives in internal/datastore, where the three readers share namespaceColumns() and TestNamespaceStore_FindByID_ReturnsEveryLifecycleState guards it.
Spec coverage
| AC | Behavior | Test |
|---|---|---|
| 7 | Anchor, slug, and derived status returned; unknown UUID → 404 |
TestResolveNamespace_EveryLifecycleState_Returns200, TestResolveNamespace_UnknownUUID_Returns404 |
| 7 | Non-canonical id → 400, over the whole alias family |
TestResolveNamespace_MalformedID_Returns400, TestResolveNamespace_NonCanonicalID_Returns400, TestParseCanonicalUUID_AcceptsExactlyTheCanonicalSpelling, TestContract_NamespaceIDParameter_MatchesHandlerGuard |
| 7 | The rule constrains spelling, not encoding: an encoded canonical id resolves | TestResolveNamespace_PercentEncodedCanonical_Resolves |
| 12 | Resolves in every state, including soft-deleted and purged | TestResolveNamespace_EveryLifecycleState_Returns200 (derived status per state); internal/datastore's TestNamespaceStore_FindByID_ReturnsEveryLifecycleState (projection per column, pre-existing) |
| 13 | Operation and every response class declared and validated | TestResolution_ResponsesMatchOpenAPIContract |
| — | Both 500s log the operation with the cause; a canceled request writes and logs nothing unless a real fault raced the hang-up |
TestResolveNamespace_StoreFailure_Returns500, TestResolveNamespace_CanceledRequest_LogAndWriteGuards |
internal/gitlabapi joins the test:integration job; its first integration test drives resolution through the real NamespaceStore, one seeded hit and one miss.
Known deviation
A method this path does not serve answers 404 from the subtree catch-all rather than 405. That is Step 4's fallback design, not this endpoint's; the 404 prose says so rather than claiming a guarantee the deployment does not make.
Related to #194 (closed)