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 <preset id=""> 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) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-09-05 21:55:20 +02:00
co-authored by Claude Opus 5
parent c46bc4898a
commit c2591f7aa5
2 changed files with 28 additions and 0 deletions
+9
View File
@@ -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
// <preset id="">. 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,
@@ -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)
}
}