feat(npm): dist-tags inline-build handler (S11 Step 10, MR 2/5)

📦 What

S11 Step 10 (dist-tags GET endpoint) is a five-MR stack, in dependency order (each targets the one above; GitLab auto-retargets to main as they merge):

  • !622 (merged) - dist-tags handler primitives + in-package unit tests.
  • !623 (merged) - inline-build handler (resolve -> 404 -> build {tag:version} -> serve). <- this MR
  • !624 (merged) - kind=1 npm_metadata_files cache fast-path + conditional-GET 304-without-blob-read.
  • !625 (merged) - inline-build singleflight (collapse concurrent cache misses).
  • !626 (merged) - integration suite (real datastore finders + stub BlobStore).

This MR targets dm/npm-local-step-10a (!622 (merged)); GitLab auto-retargets it to main when !622 (merged) merges.

Slice 2 - the handler serving GET /{slug}/npm/{repo}/-/package/{package_name}/dist-tags by building the {tag: version} map inline from npm_tags + npm_versions on every request. The kind=1 cache fast-path (!624 (merged)) and the singleflight (!625 (merged)) layer in front of this build in the follow-ups.

  • ServeHTTP validates the package name (a malformed name is 422 package_name_invalid per the S11 Validation rules, rejected before the lookup), then resolves the package via NpmPackageByName keyed on Resolution.NpmRepositoryID; a missing/soft-deleted package is 404 package_not_found (AC 34). It reads repositories.visibility for the Cache-Control directive, then inline-builds and serves with the strong "sha256-<hex>" ETag, Vary: Authorization, and the If-None-Match 304 short-circuit.
  • buildDistTags keyset-iterates npm_tags + npm_versions and renders the kind=1 Document via GenerateDistTags. A tagless package renders {} - the passive registry never synthesizes a latest tag.
  • The 404 package_not_found and 422 package_name_invalid responses reuse the transport codes (CodePackageNotFound / MsgPackageNotFound, CodePackageNameInvalid / MsgPackageNameInvalid) that landed on main with Step 9's download handler, rather than redeclaring them. The handler's datastore dependencies likewise reuse the package's existing NpmPackageFinder / tagLister / NpmVersionFinder / RepositoryVisibilityFinder finder interfaces (identical method sets), and the external test's blob-store / package-row doubles are distTags-prefixed to avoid colliding with the download suite's same-named fakes now on main.
  • The S11 spec's dist-tags GET contract is amended to document the visibility-varying Cache-Control / Vary: Authorization / ETag headers the handler emits and the 422 package_name_invalid response.
  • NewDistTagsHandler fail-fasts on any nil dependency (cache + blob deps arrive with the cache fast-path slice).

Spec coverage

Behaviour Test
Inline-build body {"<tag>":"<version>",...} for a package with tags TestDistTagsHandler_InlineBuild_BodyShapes
Tagless package -> {}, no synthesized latest (passive registry) TestDistTagsHandler_InlineBuild_BodyShapes
Cache-Control by visibility + Vary: Authorization (inline path) TestDistTagsHandler_CacheControlByVisibility
Package lookup keys on NpmRepositoryID, not RepositoryID TestDistTagsHandler_KeysPackageOnNpmRepositoryID
Constructor fail-fast on nil deps TestNewDistTagsHandler_PanicsOnNilDependencies
AC 34 - package missing / soft-deleted -> 404 package_not_found TestDistTagsHandler_PackageNotFound_404 (unit); TestDistTagsIntegration_PackageNotFound (!626 (merged), real datastore)
Malformed package name -> 422 package_name_invalid, before the lookup TestDistTagsHandler_InvalidPackageName_422
Missing resolution / package / repository / build error -> 500, no internal detail in body TestDistTagsHandler_MissingResolution_500, TestDistTagsHandler_DatastoreErrors_500

⚠️ Known gap - handler not yet wired into the route table

DistTagsHandler is implemented and tested, but nothing mounts it: internal/format/npm/handler.go still serves the 501 Not Implemented placeholder for the dist-tags route, and the tests construct the handler directly with an injected Resolution. This is an unassigned plan-level gap (the npm hosted plan creates handler.go with placeholders in Step 8 and never assigns the placeholder->real-handler swap to a handler step); it surfaces no later than Step 23 (real npm-CLI conformance) and is closed via a plan amendment, not bolted onto this stack. Deliberately out of scope.

🧪 Testing

  • go test ./internal/format/npm/... - pass
  • golangci-lint run ./internal/format/npm/... - 0 issues
  • go vet -tags integration ./internal/format/npm/... - clean (integration build compiles)

Related to #122 (closed)

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading