Don't trap when two accounts share a sourceKey

Adding the same server and username twice yields two connections with the
same sourceKey. PaginationState.rehydrate built its lookup with
Dictionary(uniqueKeysWithValues:), which traps on a duplicate key, so every
merged overview (notifications, issues/PRs, repositories) crashed on
construction. Keep the first source instead, and add a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
systemBlue 2026-06-01 14:15:56 -04:00
parent 992c628abd
commit d96a953b6f
2 changed files with 33 additions and 1 deletions

View file

@ -73,8 +73,11 @@ extension PaginationState where Item: Identifiable {
/// Replace placeholder auth services using the given connection sources.
/// Items whose sourceKey has no matching source are removed.
func rehydrate<T>(from sources: [ConnectionSource]) where Item == TaggedItem<T> {
// Two connected accounts with the same server+username share a sourceKey,
// so keep the first rather than trapping on a duplicate key.
let lookup = Dictionary(
uniqueKeysWithValues: sources.map { ($0.sourceKey, $0.authService) },
sources.map { ($0.sourceKey, $0.authService) },
uniquingKeysWith: { first, _ in first },
)
items = items.compactMap { item in
guard let auth = lookup[item.sourceKey] else { return nil }

View file

@ -190,4 +190,33 @@ struct TaggedItemTests {
#expect(pagination.items.isEmpty)
}
@MainActor
@Test func rehydrateHandlesDuplicateSourceKeys() {
// Two connected accounts with the same server+username produce duplicate
// sourceKeys; rehydrate must not trap on the duplicate key.
let liveAuth = AuthenticationService()
let key = "https://a.com:u"
let sources = [
ConnectionSource(
sourceKey: key, name: "A",
client: ForgejoClient(serverURL: "https://a.com", username: "u", token: "t"),
authService: liveAuth,
),
ConnectionSource(
sourceKey: key, name: "A (duplicate)",
client: ForgejoClient(serverURL: "https://a.com", username: "u", token: "t"),
authService: AuthenticationService(),
),
]
let pagination = PaginationState<TaggedItem<FIssue>>()
pagination.items = [
TaggedItem(item: makeIssue(id: 1), sourceKey: key, instanceName: "A", authService: makeAuth()),
]
pagination.rehydrate(from: sources)
#expect(pagination.items.count == 1)
#expect(pagination.items[0].authService === liveAuth) // first source wins
}
}