3c6b49b31c
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
81 lines
3.6 KiB
Go
81 lines
3.6 KiB
Go
package stacks
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var dA = "sha256:" + strings.Repeat("a", 64)
|
|
var dB = "sha256:" + strings.Repeat("b", 64)
|
|
|
|
func TestDigest_RenderAndStrip(t *testing.T) {
|
|
in := "# header\nservices:\n web:\n image: redis:7-alpine # pinned\n restart: unless-stopped\n db:\n image: \"mariadb:11.8\"\nvolumes:\n x:\n"
|
|
out := string(renderDigests([]byte(in), map[string]string{"web": dA, "db": "not-a-digest"}))
|
|
if !strings.Contains(out, " image: redis:7-alpine@"+dA+" # pinned") {
|
|
t.Fatalf("web not rendered:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, " image: \"mariadb:11.8\"") {
|
|
t.Fatalf("an invalid digest must leave the line alone:\n%s", out)
|
|
}
|
|
again := string(renderDigests([]byte(out), map[string]string{"web": dB}))
|
|
if strings.Count(again, "@sha256:") != 1 || !strings.Contains(again, "redis:7-alpine@"+dB) {
|
|
t.Fatalf("re-render must replace, not stack, the digest:\n%s", again)
|
|
}
|
|
imgs, err := parseComposeImagesBytes([]byte(again))
|
|
if err != nil || imgs["web"] != "redis:7-alpine" {
|
|
t.Fatalf("the parsed ref must be digest-free: %v %v", imgs, err)
|
|
}
|
|
}
|
|
|
|
// TestDigest_FloatingTagBehindOnlyForANewerTestedDigest — the badge rule, every arm.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT): make digestBehind return false — the "newer test" case then reads
|
|
// Current and this test fails.
|
|
func TestDigest_FloatingTagBehindOnlyForANewerTestedDigest(t *testing.T) {
|
|
installAt := time.Date(2026, 9, 20, 0, 0, 0, 0, time.UTC)
|
|
mk := func(instDigest, instAt string, cat map[string]string, testedAt time.Time) Stack {
|
|
return Stack{Deployed: true, CatalogImages: map[string]string{"web": "redis:7-alpine"},
|
|
CatalogDigests: cat, CatalogTestedAt: testedAt,
|
|
AppConfig: &AppConfig{InstalledImages: map[string]InstalledImage{"web": {Ref: "redis:7-alpine", Digest: instDigest, At: instAt}}}}
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
s Stack
|
|
want UpdateOrder
|
|
}{
|
|
{"same digest", mk(dA, installAt.Format(time.RFC3339), map[string]string{"web": dA}, installAt.Add(time.Hour)), UpdateOrderCurrent},
|
|
{"newer tested digest", mk(dA, installAt.Format(time.RFC3339), map[string]string{"web": dB}, installAt.Add(time.Hour)), UpdateOrderBehind},
|
|
{"test older than the install", mk(dA, installAt.Format(time.RFC3339), map[string]string{"web": dB}, installAt.Add(-time.Hour)), UpdateOrderCurrent},
|
|
{"install time unknown", mk(dA, "", map[string]string{"web": dB}, installAt.Add(time.Hour)), UpdateOrderCurrent},
|
|
{"installed digest unknown", mk("", installAt.Format(time.RFC3339), map[string]string{"web": dB}, installAt.Add(time.Hour)), UpdateOrderCurrent},
|
|
{"no tested digest", mk(dA, installAt.Format(time.RFC3339), nil, time.Time{}), UpdateOrderCurrent},
|
|
}
|
|
for _, c := range cases {
|
|
if got := CatalogOrder(c.s); got != c.want {
|
|
t.Errorf("%s: %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The update runs EXACTLY the tested image: the pinned step's compose carries the entry's digest, while
|
|
// the pin itself stays a plain ref.
|
|
func TestDigest_TheUpdateRendersTheStepsTestedDigest(t *testing.T) {
|
|
m, dir, _, _, _ := ladderManager(t, true)
|
|
catDir := filepath.Dir(m.CatalogTemplatePath("nextcloud", "docker-compose.yml"))
|
|
// the ladder fixture's digests are sha256:aaaa…
|
|
if err := m.StartGuardedUpdate("nextcloud"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
waitUpdateDone(t, m, "nextcloud")
|
|
body := fileBody(t, ComposePathIn(dir))
|
|
if !strings.Contains(body, "image: "+ladderB+"@"+dA) {
|
|
t.Fatalf("the live compose does not pin the tested digest:\n%s", body)
|
|
}
|
|
if pinOf(t, dir) != ladderB {
|
|
t.Fatalf("the pin must stay a plain ref, got %q", pinOf(t, dir))
|
|
}
|
|
_ = catDir
|
|
}
|