Repository DELETE answers 500 for a repository a virtual repository lists as an upstream
What happens
DELETE /api/v1/:slug/repositories/:repository_name answers 500 for a repository that any virtual repository lists as an upstream, under either destructive value.
RepositoryStore.Delete takes the repository row's update lock and checks the upstream listing before the cascading DELETE (lockRepositoryThenCheckUpstreams, internal/datastore/repositories.go:1028). checkRepositoryUpstreamAssociations (:1070) reads the npm junction table and returns ErrRepositoryIsVirtualUpstream (:798) when a row references the repository. Delete's doc comment lists that sentinel among its returns (:921).
managementapi.writeDeleteStoreError (internal/managementapi/delete.go:197) has no arm for it. It matches ErrRepositoryNotEmpty and ErrNotFound, and everything else falls to logAndWriteInternalError, which logs at ERROR and writes the shared 500.
ADR-009 requires a 409 here under either value. Its repository-deletion section reads that a repository any virtual repository lists as an upstream "cannot be deleted under either value, and the request returns 409 Conflict. Destructive intent does not reach it", and that "The 409 names the condition without listing the referencing virtual repositories." S17 AC #110 (closed) carries the same criterion. The same section names this outcome as the reason the API layer owns the translation at all: the referential action alone "either fails as a statement the API layer has to translate, or aborts a background purge that reaches no caller".
This is the defect Repository DELETE answers 500 for the virtual k... (#819 - closed) • Unassigned recorded for the virtual kind, one layer up: a typed datastore sentinel with no handler arm.
Why no caller can reach it yet
The refusal needs a junction row. The npm junction store exists and can write one (internal/datastore/npm_virtual_upstreams.go:685), but nothing reaches that insert: no upstream-association route is registered, no handler for the resource exists under internal/managementapi/, and the store is not wired into cmd/artifact-registry. Only a direct database write produces a row today.
The window opens with the association routes, which docs/plans/2026-08-13-s17-phase6-virtual-remote-repositories.md assigns to Steps 40 through 42. Whichever step registers the first association route makes the 500 reachable for npm, so the arm belongs in that step or ahead of it.
What a fix has to do
- Add the arm. The sentinel is not
ErrRepositoryNotEmpty, so it already bypasses the handler'sdestructive && ErrRepositoryNotEmptytombstone arm and lands inwriteDeleteStoreError's default. The whole fix is onecase errors.Is(err, datastore.ErrRepositoryIsVirtualUpstream)returning409, which then holds under bothdestructivevalues without a second branch. - Decide the message. ADR-009 wants the
409to name the condition without listing the referencing virtual repositories.nonEmptyConflictMessagepicks between two not-empty remedies and neither fits, so this needs a third static message pointing at dissociation. - Correct a comment the same change falsifies.
writeDeleteStoreError's doc says "the default 500 covers the virtual-kind refusal, reachable only for a format outside the taxonomy". The default also covers the upstream refusal, which is reachable for npm as soon as a junction row exists. - Cover the handler. The datastore half is covered (
npm_virtual_repositories_integration_test.go:440,npm_virtual_upstreams_integration_test.go:673and:762). The handler half has no test, and a unit test over the existing fakeRepositoryDeleterreturning the sentinel is enough to pin the status and the message under bothdestructivevalues.
Adjacent, not the same
Virtual upstream association can commit between... (#1373) • Unassigned covers a race between the guarded delete's rollback and the destructive soft-delete, where an association commits in the gap and leaves a tombstone the purge cannot finish. That gap is about when the check runs. This one is about what the caller is told when the check fires.