test: cover the nested-repo tag rkey on the delete paths

c035f50 fixed a hand-built tag rkey in DeleteManifestHandler and shipped with
no test. The hazard is not specific to that handler: io.atcr.tag rkeys come
from RepositoryTagToRKey, which encodes "/" as "~", so any code building one by
hand targets a record that does not exist — and deleteRecord being idempotent
makes that a silent no-op. The local view looks right and the tag returns on
the next backfill.

The by-digest path now builds tag rkeys too (594d73b), so it could reintroduce
exactly this bug. TestManifestDelete_NestedRepoTagRKey pins it there: push to
stream/cache, delete by digest, and assert the tag is no longer listed.
Listing is what catches a survivor — TagStore.All reads the records back from
the PDS and filters by repository, so a stale one is still reported.

Mutation-verified by hand-building the rkey as "repo:tag": the nested test
fails with the tag still listed, and TestManifestDelete passes unchanged. That
second half is the point — every existing delete test uses a flat repository
name, and a flat name cannot reproduce this bug at all.

batch11-nested-rkey.mjs drives the same property through the UI handler that
c035f50 actually fixed, asserting against the PDS record rather than the page,
since the page looks correct either way until a backfill runs. It needs an
interactive appview login in the Playwright profile and is not yet run; the
session that exists belongs to a different browser profile. Two instrument
notes are baked in: probe /settings rather than the repo page to detect a
session, because /r/ renders for anonymous visitors and can never report a
missing one, and use maxRedirects:0, because RequireAuth 302s and a followed
redirect surfaces as a confusing 405 on DELETE /login.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh
This commit is contained in:
Evan Jarrett
2026-08-25 16:34:25 -05:00
co-authored by Claude Opus 5
parent 0b212a527f
commit b17ebb69a5
2 changed files with 145 additions and 0 deletions
+59
View File
@@ -5,6 +5,7 @@ package integration
import (
"context"
"fmt"
"slices"
"testing"
"github.com/google/go-containerregistry/pkg/crane"
@@ -173,3 +174,61 @@ func TestManifestDelete_SharedDigestAcrossRepos(t *testing.T) {
t.Fatalf("repo B is no longer pullable after repo A was deleted: %v", err)
}
}
// TestManifestDelete_NestedRepoTagRKey covers the encoding hazard c035f50 fixed
// in the UI handler, on the by-digest path.
//
// io.atcr.tag rkeys come from RepositoryTagToRKey, which encodes "/" as "~", so
// stream/cache:v1 is stored as "stream~cache_v1". Any code that hand-builds the
// rkey ("repo:tag", "repo_tag") targets a record that does not exist, and
// because deleteRecord is idempotent it fails silently: the local view looks
// right and the tag comes back on the next backfill.
//
// A flat repository name cannot reproduce it — the bug is entirely in the "/"
// encoding — which is why every earlier delete test missed it. Listing tags
// after the delete is what catches a surviving record: TagStore.All reads the
// records back from the PDS and filters by repository, so a stale one is still
// reported.
func TestManifestDelete_NestedRepoTagRKey(t *testing.T) {
h := testharness.New(t)
alice := h.AddSailor("alice.test")
creds := h.RegistryCreds(alice)
authOpts := []crane.Option{crane.WithAuth(toAuthn(creds)), crane.Insecure}
repo := fmt.Sprintf("%s/%s/stream/cache", h.AppViewHostPort(), alice.Handle())
tagRef := repo + ":v1"
img, err := random.Image(1<<20, 2)
if err != nil {
t.Fatalf("build image: %v", err)
}
if err := crane.Push(img, tagRef, authOpts...); err != nil {
t.Fatalf("push nested: %v", err)
}
tags, err := crane.ListTags(repo, authOpts...)
if err != nil {
t.Fatalf("list tags before delete: %v", err)
}
if !slices.Contains(tags, "v1") {
t.Fatalf("fixture is wrong: v1 not listed before delete, got %v", tags)
}
dgst, err := img.Digest()
if err != nil {
t.Fatalf("digest: %v", err)
}
if err := crane.Delete(fmt.Sprintf("%s@%s", repo, dgst.String()), authOpts...); err != nil {
t.Fatalf("delete nested by digest: %v", err)
}
tags, err = crane.ListTags(repo, authOpts...)
if err != nil {
t.Fatalf("list tags after delete: %v", err)
}
if slices.Contains(tags, "v1") {
t.Errorf("tag v1 survived the delete on a nested repository: %v — the tag rkey did not match "+
"the slash-encoded key the write path uses, so the record is still on the PDS and will "+
"reappear on the next backfill", tags)
}
}