From c2591f7aa5e0f2cde779b615b85753163d516cd2 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sat, 5 Sep 2026 21:40:08 +0200 Subject: [PATCH] fix(setup): ignore cleared preset slots in the readiness comparison Clearing a slot through the Marge API leaves a zero-value entry in the list (RemovePreset assigns models.ServicePreset{}), which savePresetsNoLock persists as with no filtering. Reading it back gives an empty slot, while mapPresetsToFullResponse drops it from the rendered /full. The comparison then refused migration with either "contains a preset without a slot" or a count mismatch, for a datastore that was otherwise perfectly in sync, and Data Sync could not fix it because nothing was actually wrong. An empty slot carries no identity to compare, so skip it on both sides. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/service/setup/migration_readiness.go | 9 +++++++++ pkg/service/setup/migration_readiness_test.go | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pkg/service/setup/migration_readiness.go b/pkg/service/setup/migration_readiness.go index 8aa47da0..f8abcfe2 100644 --- a/pkg/service/setup/migration_readiness.go +++ b/pkg/service/setup/migration_readiness.go @@ -153,6 +153,15 @@ func migrationPresetIdentities(presets []models.ServicePreset) []migrationPreset slot = presets[i].ID } + // Clearing a slot through the Marge API leaves a zero-value entry in + // the list (RemovePreset assigns models.ServicePreset{}), persisted as + // . The rendered /full drops it, so counting it here + // would report a mismatch for a datastore that is perfectly in sync. + // An empty slot carries no identity to compare either way. + if slot == "" { + continue + } + result = append(result, migrationPresetIdentity{ Slot: slot, Name: presets[i].Name, diff --git a/pkg/service/setup/migration_readiness_test.go b/pkg/service/setup/migration_readiness_test.go index d04ae297..767fa47f 100644 --- a/pkg/service/setup/migration_readiness_test.go +++ b/pkg/service/setup/migration_readiness_test.go @@ -284,3 +284,22 @@ func TestMigrationDataReadinessAllowsUnpairedSpeaker(t *testing.T) { t.Fatalf("unpaired speaker was refused migration: %v", err) } } + +// TestMigrationDataReadinessIgnoresClearedPresetSlots: clearing a slot through +// the Marge API leaves a zero-value entry that /full drops. Counting it would +// refuse migration for a datastore that is otherwise perfectly in sync. +func TestMigrationDataReadinessIgnoresClearedPresetSlots(t *testing.T) { + kept := readinessPreset("1", "Kept Station", "http://radio.example/kept") + + m, ds, deviceIP := newMigrationReadinessFixture(t, livePresetsXML(kept)) + if err := ds.SavePresets(readinessAccount, readinessDevice, []models.ServicePreset{ + kept, + {}, // slot 2, cleared through RemovePreset + }); err != nil { + t.Fatalf("SavePresets: %v", err) + } + + if _, err := m.checkMigrationDataReady(deviceIP); err != nil { + t.Fatalf("a cleared preset slot blocked migration: %v", err) + } +}