From 8624c37041a47247a8f4642674334395ad223888 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:22:13 +0200 Subject: [PATCH] fix(profiles): notice a corrected connection creation time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ActiveProfileProvider diffed connections on toConfigJson alone, but createdAt is a real column and now decides which connection lends a profile its picture. A creation-time correction was therefore invisible to the guard and left a stale avatar until the next launch. Compare createdAt alongside the config. ConnectionRegistry pins creation order across re-authentication, so this adds no notifications in normal operation — it only stops an out-of-band correction, such as a restore or a backfill, from being swallowed. --- lib/profiles/active_profile_provider.dart | 12 +++++- .../active_profile_provider_test.dart | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/profiles/active_profile_provider.dart b/lib/profiles/active_profile_provider.dart index ee0e3dea..8eaa2011 100644 --- a/lib/profiles/active_profile_provider.dart +++ b/lib/profiles/active_profile_provider.dart @@ -217,14 +217,22 @@ class ActiveProfileProvider extends ChangeNotifier with DisposableChangeNotifier _resolveActive(); } - /// [Connection] has no value equality; its persisted config is the - /// cheapest faithful comparison key for the handful of rows involved. + /// [Connection] has no value equality; its persisted config is the cheapest + /// faithful comparison key for the handful of rows involved. + /// + /// `createdAt` is compared separately because it is a real column rather + /// than part of `toConfigJson`, and avatar selection reads it: a profile + /// shows the picture of its oldest linked connection. `ConnectionRegistry` + /// pins creation order across re-authentication, so this costs no extra + /// notifications — it only stops a genuine correction (a restore, a + /// backfill) from being swallowed until the next launch. static bool _sameConnections(Map a, Map b) { if (a.length != b.length) return false; for (final entry in a.entries) { final other = b[entry.key]; if (other == null) return false; if (identical(entry.value, other)) continue; + if (entry.value.createdAt != other.createdAt) return false; if (jsonEncode(entry.value.toConfigJson()) != jsonEncode(other.toConfigJson())) return false; } return true; diff --git a/test/profiles/active_profile_provider_test.dart b/test/profiles/active_profile_provider_test.dart index 9fdb7cd0..5be1398d 100644 --- a/test/profiles/active_profile_provider_test.dart +++ b/test/profiles/active_profile_provider_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:drift/drift.dart' show Value; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/connection/connection.dart'; @@ -305,6 +306,45 @@ void main() { ); }); + test('a corrected connection creation time re-picks the avatar without a restart', () async { + // createdAt is a real column, not part of toConfigJson, so the + // connection diff guard has to compare it explicitly. Nothing in + // normal operation rewrites it — ConnectionRegistry pins creation + // order across re-auth — but a restore or a backfill can, and + // swallowing that would leave a stale avatar until the next launch. + final profile = Profile.local(id: 'p1', displayName: 'Owner', createdAt: DateTime(2026, 1, 1)); + await registry.upsert(profile); + await connections.upsert(_jellyfin('a', createdAt: DateTime(2025, 1, 1), primaryImageTag: 'a-tag')); + await connections.upsert(_jellyfin('b', createdAt: DateTime(2026, 1, 1), primaryImageTag: 'b-tag')); + await profileConnections.upsert( + const ProfileConnection(profileId: 'p1', connectionId: 'a', userIdentifier: 'user-a'), + ); + await profileConnections.upsert( + const ProfileConnection(profileId: 'p1', connectionId: 'b', userIdentifier: 'user-b'), + ); + await provider.initialize(); + expect(provider.avatarUrlFor(profile.id), contains('tag=a-tag')); + + final changed = Completer(); + void listener() { + if ((provider.avatarUrlFor(profile.id)?.contains('b-tag') ?? false) && !changed.isCompleted) { + changed.complete(); + } + } + + provider.addListener(listener); + addTearDown(() => provider.removeListener(listener)); + + // Straight to the table: the registry deliberately refuses to restamp + // an existing row, so this stands in for an out-of-band correction. + await (db.update(db.connections)..where((t) => t.id.equals('a'))).write( + ConnectionsCompanion(createdAt: Value(DateTime(2027, 1, 1).millisecondsSinceEpoch)), + ); + await changed.future.timeout(const Duration(seconds: 2)); + + expect(provider.avatarUrlFor(profile.id), contains('tag=b-tag')); + }); + test('token and timestamp churn on a link does not notify listeners', () async { await registry.upsert(Profile.local(id: 'p1', displayName: 'Owner', createdAt: DateTime(2026, 1, 1))); await connections.upsert(_jellyfin('jellyfin', createdAt: DateTime(2025, 1, 1), primaryImageTag: 'avatar-tag'));