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) + } +}