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_filescache 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.
ServeHTTPvalidates the package name (a malformed name is 422package_name_invalidper the S11 Validation rules, rejected before the lookup), then resolves the package viaNpmPackageByNamekeyed onResolution.NpmRepositoryID; a missing/soft-deleted package is 404package_not_found(AC 34). It readsrepositories.visibilityfor theCache-Controldirective, then inline-builds and serves with the strong"sha256-<hex>"ETag,Vary: Authorization, and theIf-None-Match304 short-circuit.buildDistTagskeyset-iteratesnpm_tags+npm_versionsand renders the kind=1DocumentviaGenerateDistTags. A tagless package renders{}- the passive registry never synthesizes alatesttag.- The 404
package_not_foundand 422package_name_invalidresponses reuse thetransportcodes (CodePackageNotFound/MsgPackageNotFound,CodePackageNameInvalid/MsgPackageNameInvalid) that landed onmainwith Step 9's download handler, rather than redeclaring them. The handler's datastore dependencies likewise reuse the package's existingNpmPackageFinder/tagLister/NpmVersionFinder/RepositoryVisibilityFinderfinder interfaces (identical method sets), and the external test's blob-store / package-row doubles aredistTags-prefixed to avoid colliding with the download suite's same-named fakes now onmain. - The S11 spec's dist-tags GET contract is amended to document the visibility-varying
Cache-Control/Vary: Authorization/ETagheaders the handler emits and the 422package_name_invalidresponse. NewDistTagsHandlerfail-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/...- passgolangci-lint run ./internal/format/npm/...- 0 issuesgo vet -tags integration ./internal/format/npm/...- clean (integration build compiles)
Related to #122 (closed)