diff --git a/pkg/service/setup/migration_readiness.go b/pkg/service/setup/migration_readiness.go index 2ae36ca5..75cd8de8 100644 --- a/pkg/service/setup/migration_readiness.go +++ b/pkg/service/setup/migration_readiness.go @@ -58,7 +58,16 @@ func (m *Manager) checkMigrationDataReady(deviceIP string) error { } if accountID == "" { - return migrationDataNotReadyf("live /info has no paired margeAccountUUID") + // An unpaired speaker, typically factory-reset, has no account data to + // preserve, so there is nothing for this check to compare and nothing + // to lose. Refusing here would also break the documented onboarding + // order: the admin UI migrates first and pairs afterwards (see the + // "Pairing runs after the URL flip" comment in the setup page), and + // MIGRATION-GUIDE.md tells the user to Generate an account ID on a + // factory-reset device. Data Sync cannot unblock it either, since it + // files an account-less device under "default", which never matches an + // empty live account. + return nil } if !datastore.IsSafeIdentifier(accountID) || !datastore.IsSafeIdentifier(deviceID) { diff --git a/pkg/service/setup/migration_readiness_test.go b/pkg/service/setup/migration_readiness_test.go index 767e13d5..77d01b85 100644 --- a/pkg/service/setup/migration_readiness_test.go +++ b/pkg/service/setup/migration_readiness_test.go @@ -252,3 +252,27 @@ func assertPresetSnapshotUnchanged(t *testing.T, path string, want []byte) { t.Fatalf("readiness preflight rewrote Presets.xml\n got: %s\nwant: %s", after, want) } } + +// TestMigrationDataReadinessAllowsUnpairedSpeaker: a factory-reset speaker has +// no account data to preserve, and the admin UI migrates before it pairs, so +// refusing here would make onboarding impossible. Data Sync could not unblock +// it either: an account-less device is filed under "default", which never +// matches an empty live account. +func TestMigrationDataReadinessAllowsUnpairedSpeaker(t *testing.T) { + speaker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/info" { + _, _ = fmt.Fprintf(w, `Fresh Speaker`, readinessDevice) + + return + } + + http.NotFound(w, r) + })) + defer speaker.Close() + + m := NewManager("http://aftertouch.example:8000", datastore.NewDataStore(t.TempDir()), nil) + + if err := m.checkMigrationDataReady(strings.TrimPrefix(speaker.URL, "http://")); err != nil { + t.Fatalf("unpaired speaker was refused migration: %v", err) + } +}