v0.53.1: refresh recovery units on periodic cache cycle (idempotent)
CaptureRecoveryUnit now builds content in memory and skips writes when the unit is already current (checksum + dump-set + version), so it can run from RefreshCache (startup + every 5m) without thrashing the USB drive. Units now exist shortly after startup and track config changes without waiting for the daily DB dump. +idempotency test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,52 @@ func TestCaptureRecoveryUnitIsSecretFree(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestCaptureRecoveryUnitIdempotent proves the checksum-skip guard: a second capture with unchanged
|
||||
// config does NOT rewrite the manifest (CreatedAt stable), but a config change DOES.
|
||||
func TestCaptureRecoveryUnitIdempotent(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
stackDir := filepath.Join(tmp, "stack")
|
||||
drive := filepath.Join(tmp, "drive")
|
||||
mustWrite(t, filepath.Join(stackDir, "docker-compose.yml"), "services:\n app:\n image: ex/app:1\n")
|
||||
mustWrite(t, filepath.Join(AppDBDumpPath(drive, "ex"), "ex.sql"), "d")
|
||||
|
||||
info := RecoveryInfo{StackDir: stackDir, DisplayName: "Ex", ImagePins: []string{"ex/app:1"},
|
||||
NonSecretEnv: map[string]string{"SUBDOMAIN": "ex"}}
|
||||
m := &Manager{logger: log.New(io.Discard, "", 0), systemDataPath: filepath.Join(tmp, "sys"),
|
||||
stackProvider: &fakeRecoveryProvider{info: info, hdd: drive}, version: "v1"}
|
||||
|
||||
manifestPath := RecoveryUnitManifestPath(drive, "ex")
|
||||
if err := m.CaptureRecoveryUnit("ex"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := readManifest(manifestPath)
|
||||
if first == nil {
|
||||
t.Fatal("manifest not written")
|
||||
}
|
||||
|
||||
// Second capture, unchanged → skipped (manifest byte-identical incl. CreatedAt).
|
||||
if err := m.CaptureRecoveryUnit("ex"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again := readManifest(manifestPath); again.CreatedAt != first.CreatedAt {
|
||||
t.Errorf("idempotent capture rewrote manifest: %q -> %q", first.CreatedAt, again.CreatedAt)
|
||||
}
|
||||
|
||||
// Change the compose → must rewrite (config checksum differs).
|
||||
mustWrite(t, filepath.Join(stackDir, "docker-compose.yml"), "services:\n app:\n image: ex/app:2\n")
|
||||
m.stackProvider.(*fakeRecoveryProvider).info.ImagePins = []string{"ex/app:2"}
|
||||
if err := m.CaptureRecoveryUnit("ex"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changed := readManifest(manifestPath)
|
||||
if len(changed.ImagePins) != 1 || changed.ImagePins[0] != "ex/app:2" {
|
||||
t.Errorf("config change not captured: %v", changed.ImagePins)
|
||||
}
|
||||
if changed.Checksums["docker-compose.yml"] == first.Checksums["docker-compose.yml"] {
|
||||
t.Errorf("compose checksum should change after edit")
|
||||
}
|
||||
}
|
||||
|
||||
func mustWrite(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user