2d63714eca
Every night leg (DB dump, volume dump, recovery-unit capture, Tier-2 mirror) now leaves alone an app whose app.yaml pin (pinned_images) differs from its running record (installed_images) - the state a failed update leaves behind. A hold lifted by hand (--clear-restore-hold + restart) no longer lets the capture write the just-failed definition over the good unit. Unknown (no pin, no record, a service not observed) never skips. The log says it per leg; the backups page shows one amber line, hu + en (backup.status.version_skip). Seam: backup.Manager.SetVersionCheck <- stacks.Manager.PinNotRunning. Tests: TestR645_HandLiftedHoldKeepsTheGoodUnit (whole night run + Tier 2, unit tree fingerprint), TestR645_VersionSkipSentence, TestR645_PinNotRunning_*, TestR645_BackupRowSaysTheNightBackupSkipsIt, TestR645_VersionCheckIsWiredAtStartup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
40 lines
2.0 KiB
Go
40 lines
2.0 KiB
Go
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")
|
|
}
|
|
}
|