package stacks import ( "strings" "testing" ) // R-645 (09 §3 decision 142) — the night backup asks "is this app running its pinned version?". // These pin the answer; r645_version_skip_test.go (backup) pins what the backup does with it. // The measured shape (9202, 2026-09-23): a failed update leaves the pin on the new version and the // running record on the old one. Read from the app.yaml ON DISK, not the in-memory copy. func TestR645_PinNotRunning_FailedUpdateShapeIsAMismatch(t *testing.T) { appYAML := "deployed: true\npinned_images:\n web: nextcloud:34.0.1-apache\ninstalled_images:\n web:\n ref: nextcloud:31.0.14-apache\n" m, _ := newPinManager(t, pinTplNew, "", appYAML) m.stacks["nextcloud"].AppConfig = nil // the in-memory view must not be what answers why, skip := m.PinNotRunning("nextcloud") if !skip || !strings.Contains(why, "web runs nextcloud:31.0.14-apache, pinned nextcloud:34.0.1-apache") { t.Fatalf("PinNotRunning = (%q, %v), want a mismatch naming both versions", why, skip) } } func TestR645_PinNotRunning_AgreementAndUnknownAreNotAMismatch(t *testing.T) { for name, appYAML := range map[string]string{ "agree": "deployed: true\npinned_images:\n web: nextcloud:31.0.14-apache\ninstalled_images:\n web:\n ref: nextcloud:31.0.14-apache\n", "unpinned": "deployed: true\ninstalled_images:\n web:\n ref: nextcloud:31.0.14-apache\n", "no running record": "deployed: true\npinned_images:\n web: nextcloud:34.0.1-apache\n", "service unobserved": "deployed: true\npinned_images:\n web: nextcloud:34.0.1-apache\ninstalled_images:\n db:\n ref: postgres:16\n", } { m, _ := newPinManager(t, pinTplOld, "", appYAML) if why, skip := m.PinNotRunning("nextcloud"); skip { t.Errorf("%s: unknown or agreeing must never skip a backup, got (%q, true)", name, why) } } m, _ := newPinManager(t, pinTplOld, "", "deployed: true\n") if _, skip := m.PinNotRunning("no-such-app"); skip { t.Error("an unknown app is not a mismatch") } }