From ee7ecbd928a397731a6afe176cd7036d92282d80 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:23:52 +0100 Subject: [PATCH] test(docker): cover multi-RepoDigest update comparison Restore the remaining focused regressions for the all-local-RepoDigests update comparison carried on the earlier core-runtime candidate: multi-digest resolution in getImageRepoDigests, false-update suppression in CheckImageUpdate, and the DigestsDiffer multi-digest cases. Test-only additions; the runtime change and contract sections are in the preceding fix(docker) commit. Change-source: pulse-maintainer --- .../dockeragent/agent_image_digest_test.go | 36 +++++++++++++++++ .../dockeragent/registry_manifest_test.go | 40 +++++++++++++++++++ internal/dockeragent/registry_test.go | 18 +++++++++ 3 files changed, 94 insertions(+) diff --git a/internal/dockeragent/agent_image_digest_test.go b/internal/dockeragent/agent_image_digest_test.go index f48c12cc58..ff96df38f4 100644 --- a/internal/dockeragent/agent_image_digest_test.go +++ b/internal/dockeragent/agent_image_digest_test.go @@ -77,6 +77,42 @@ func TestAgent_getImageRepoDigest_FallbackToFirst(t *testing.T) { } } +func TestAgent_getImageRepoDigests_MultipleDigestsForOneImage(t *testing.T) { + agent := &Agent{ + docker: &fakeDockerClient{ + imageInspectWithRawFn: func(ctx context.Context, imageID string) (image.InspectResponse, []byte, error) { + return image.InspectResponse{ + RepoDigests: []string{ + "docker.io/library/postgres@sha256:44c4", + "docker.io/library/postgres@sha256:cf78", + "docker.io/other/postgres@sha256:beef", + }, + Architecture: "amd64", + Os: "linux", + }, nil, nil + }, + }, + logger: zerolog.New(io.Discard), + } + + digests, arch, os, variant := agent.getImageRepoDigests(context.Background(), "image-id", "postgres:16.15-alpine3.24") + want := []string{"sha256:44c4", "sha256:cf78", "sha256:beef"} + if len(digests) != len(want) { + t.Fatalf("digests = %v, want %v", digests, want) + } + for i := range want { + if digests[i] != want[i] { + t.Fatalf("digests = %v, want %v", digests, want) + } + } + if arch != "amd64" || os != "linux" || variant != "" { + t.Fatalf("platform = %q/%q/%q, want amd64/linux/", arch, os, variant) + } + if got, _, _, _ := agent.getImageRepoDigest(context.Background(), "image-id", "postgres:16.15-alpine3.24"); got != "sha256:44c4" { + t.Fatalf("primary digest = %q, want sha256:44c4", got) + } +} + func TestAgent_getImageRepoDigest_InvalidRepoDigest(t *testing.T) { agent := &Agent{ docker: &fakeDockerClient{ diff --git a/internal/dockeragent/registry_manifest_test.go b/internal/dockeragent/registry_manifest_test.go index 79379826e9..c2010b36c4 100644 --- a/internal/dockeragent/registry_manifest_test.go +++ b/internal/dockeragent/registry_manifest_test.go @@ -123,3 +123,43 @@ func TestRegistryChecker_MultiArchDigestLayersStayConsistentAcrossCache(t *testi t.Fatalf("expected one HEAD and one GET before the cache hit, got %d requests", requestCount) } } + +// Issue #2110: a local image can carry several RepoDigests, for example the +// tag's manifest digest and a platform manifest digest. The update check must +// treat any of them as the current image rather than reporting a false update +// just because the first entry differs from the registry digest. +func TestRegistryChecker_MultipleLocalRepoDigestsSuppressFalseUpdate(t *testing.T) { + checker := NewRegistryChecker(zerolog.Nop()) + checker.httpClient = &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.Method == http.MethodHead { + return newStringResponse(http.StatusOK, map[string]string{ + "Content-Type": "application/vnd.docker.distribution.manifest.v2+json", + "Docker-Content-Digest": "sha256:cf78", + }, ""), nil + } + return newStringResponse(http.StatusOK, nil, "{}"), nil + }), + } + + result := checker.CheckImageUpdate( + context.Background(), + "example.test/postgres:16.15-alpine3.24", + "sha256:44c4,sha256:cf78", + "amd64", + "linux", + "", + ) + if result == nil { + t.Fatal("expected an update result") + } + if result.UpdateAvailable { + t.Fatalf("a local RepoDigest matching the registry digest must suppress the update: %#v", result) + } + if result.CurrentDigest != "sha256:44c4" { + t.Fatalf("primary current digest = %q, want %q", result.CurrentDigest, "sha256:44c4") + } + if result.LatestDigest != "sha256:cf78" { + t.Fatalf("latest digest = %q, want %q", result.LatestDigest, "sha256:cf78") + } +} diff --git a/internal/dockeragent/registry_test.go b/internal/dockeragent/registry_test.go index 7b0a7007a3..7624a32d35 100644 --- a/internal/dockeragent/registry_test.go +++ b/internal/dockeragent/registry_test.go @@ -207,6 +207,24 @@ func TestRegistryChecker_DigestsDiffer(t *testing.T) { latest: "def456,abc123", want: true, // No match found }, + { + name: "multiple current digests match second latest", + current: "sha256:44c4,sha256:cf78", + latest: "sha256:cf78", + want: false, // One local RepoDigest is current + }, + { + name: "multiple current digests no match", + current: "sha256:44c4,sha256:aaaa", + latest: "sha256:cf78", + want: true, // No local RepoDigest is current + }, + { + name: "empty entries in current set are ignored", + current: " ,sha256:cf78", + latest: "sha256:cf78", + want: false, + }, } for _, tt := range tests {