diff --git a/lib/connection/connection_registry.dart b/lib/connection/connection_registry.dart index df894293..eb692b47 100644 --- a/lib/connection/connection_registry.dart +++ b/lib/connection/connection_registry.dart @@ -42,14 +42,22 @@ class ConnectionRegistry { /// Insert or replace [connection]. If this is the first stored connection /// it is automatically marked default; re-upserting an existing row keeps - /// the row's current `isDefault` (so token/metadata refreshes don't clear - /// the default flag). + /// the row's current `isDefault` and `createdAt` (so token/metadata + /// refreshes don't clear the default flag or restamp creation order). + /// + /// Creation order is behaviour, not bookkeeping: it decides which + /// connection lends a profile its picture, and `remove` promotes the oldest + /// remaining row to default. Re-authenticating rebuilds the model with + /// `DateTime.now()` and reuses the same stable id, so without this the + /// originally-first connection would jump to last on every re-sign-in. Future upsert(Connection connection) async { await _db.runIdentityMutation(() async { final existing = await (_db.select(_db.connections)..where((t) => t.id.equals(connection.id))).getSingleOrNull(); final bool isDefault; + final int createdAt; if (existing != null) { isDefault = existing.isDefault; + createdAt = existing.createdAt; } else { final any = await (_db.selectOnly(_db.connections) @@ -57,6 +65,7 @@ class ConnectionRegistry { ..limit(1)) .getSingleOrNull(); isDefault = any == null; + createdAt = connection.createdAt.millisecondsSinceEpoch; } final protectedConfig = await CredentialVault.protectConnectionConfig( connection.kind.id, @@ -68,7 +77,7 @@ class ConnectionRegistry { displayName: Value(connection.displayName), configJson: Value(jsonEncode(protectedConfig)), isDefault: Value(isDefault), - createdAt: Value(connection.createdAt.millisecondsSinceEpoch), + createdAt: Value(createdAt), lastAuthenticatedAt: Value(connection.lastAuthenticatedAt?.millisecondsSinceEpoch), ); await _db.into(_db.connections).insertOnConflictUpdate(row); diff --git a/test/connection/connection_registry_test.dart b/test/connection/connection_registry_test.dart index fda42bc4..1a50beac 100644 --- a/test/connection/connection_registry_test.dart +++ b/test/connection/connection_registry_test.dart @@ -20,7 +20,7 @@ Future _defaultConnectionId(AppDatabase db) async { return null; } -JellyfinConnection _jellyfin({String id = 'srv-1', String userName = 'edde'}) { +JellyfinConnection _jellyfin({String id = 'srv-1', String userName = 'edde', int createdAtMs = 1_000_000}) { return JellyfinConnection( id: id, baseUrl: 'https://jellyfin.local', @@ -30,7 +30,7 @@ JellyfinConnection _jellyfin({String id = 'srv-1', String userName = 'edde'}) { userName: userName, accessToken: 'tok-$id', deviceId: 'dev-1', - createdAt: DateTime.fromMillisecondsSinceEpoch(1_000_000), + createdAt: DateTime.fromMillisecondsSinceEpoch(createdAtMs), ); } @@ -184,6 +184,28 @@ void main() { expect(await _defaultConnectionId(db), 'a'); }); + test('re-upsert preserves the original creation order', () async { + // Regression: creation order decides which connection lends a profile + // its picture (issue #1667) and which row `remove` promotes to default. + // Re-authenticating rebuilds the model with `DateTime.now()` under the + // same stable id, so an unguarded writer restamped the originally-first + // connection and shuffled it to last. + await registry.upsert(_jellyfin(id: 'first', createdAtMs: 1_000_000)); + await registry.upsert(_jellyfin(id: 'second', createdAtMs: 2_000_000)); + + await registry.upsert(_jellyfin(id: 'first', createdAtMs: 9_000_000)); + + final list = await registry.list(); + expect(list.map((c) => c.id).toList(), ['first', 'second']); + expect(list.first.createdAt, DateTime.fromMillisecondsSinceEpoch(1_000_000)); + }); + + test('a genuinely new connection keeps the creation time it was built with', () async { + await registry.upsert(_jellyfin(id: 'a', createdAtMs: 5_000_000)); + + expect((await registry.list()).single.createdAt, DateTime.fromMillisecondsSinceEpoch(5_000_000)); + }); + test('recordAuthSuccess updates lastAuthenticatedAt without losing config', () async { await registry.upsert(_jellyfin(id: 'a')); final at = DateTime.fromMillisecondsSinceEpoch(2_000_000);