From 84ff4d57a4a03ce3dfe105fab3b25c95bc45c0e5 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:45:47 -0400 Subject: [PATCH] fix(bar): tolerate duplicate rows in alert pruning (#1553) Bar alert pruning tolerates duplicate rows without crashing. --- macos-bar/Sources/CCSBarCheck/main.swift | 29 +++++++++++++++++++ .../Sources/CCSBarCore/BarAlertEngine.swift | 7 +++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/macos-bar/Sources/CCSBarCheck/main.swift b/macos-bar/Sources/CCSBarCheck/main.swift index f11e823c..f11dbfd7 100644 --- a/macos-bar/Sources/CCSBarCheck/main.swift +++ b/macos-bar/Sources/CCSBarCheck/main.swift @@ -758,6 +758,35 @@ do { check(t1.firedKeys.isEmpty, "prune collapses stale-bucket + absent-account keys (bounded)") } +// (H) Duplicate account ids from a malformed summary must not crash pruning. +do { + let rows = [ + BarSummaryRow( + accountId: "dup@example.com", provider: "agy", quotaPercentage: 5, quotaStatus: "ok", + nextReset: "2026-07-01T00:00:00Z"), + BarSummaryRow( + accountId: "dup@example.com", provider: "agy", quotaPercentage: 4, quotaStatus: "ok", + nextReset: "2026-08-01T00:00:00Z"), + ] + let prior: Set = [ + "quotaRemainingBelow|agy:dup@example.com|2026-07-01T00:00:00Z|L10", + "quotaRemainingBelow|agy:dup@example.com|2026-08-01T00:00:00Z|L10", + "quotaRemainingBelow|agy:dup@example.com|2026-09-01T00:00:00Z|L10", + ] + let ev = BarAlertEngine.evaluate( + rows: rows, analytics: nil, prefs: BarAlertPrefs(quotaLevels: [10]), priorFiredKeys: prior, + now: engineNow, calendar: utc) + check( + ev.firedKeys.contains("quotaRemainingBelow|agy:dup@example.com|2026-07-01T00:00:00Z|L10"), + "duplicate ids: prune keeps first present reset bucket") + check( + ev.firedKeys.contains("quotaRemainingBelow|agy:dup@example.com|2026-08-01T00:00:00Z|L10"), + "duplicate ids: prune keeps second present reset bucket") + check( + !ev.firedKeys.contains("quotaRemainingBelow|agy:dup@example.com|2026-09-01T00:00:00Z|L10"), + "duplicate ids: prune drops absent reset bucket") +} + // (H) Deterministic order: shuffled rows produce notifs in stable id order. do { let rows = [ diff --git a/macos-bar/Sources/CCSBarCore/BarAlertEngine.swift b/macos-bar/Sources/CCSBarCore/BarAlertEngine.swift index 2b35f2d9..132e2c55 100644 --- a/macos-bar/Sources/CCSBarCore/BarAlertEngine.swift +++ b/macos-bar/Sources/CCSBarCore/BarAlertEngine.swift @@ -251,8 +251,9 @@ public enum BarAlertEngine { // PRUNE — keep the fired set bounded so it can't grow without limit across // day/month/reset rollovers or account churn. let presentIds = Set(rows.map { $0.id }) - let presentResetBuckets: [String: String] = Dictionary( - uniqueKeysWithValues: rows.map { ($0.id, $0.nextReset ?? "noreset") }) + let presentResetBuckets: [String: Set] = rows.reduce(into: [:]) { buckets, row in + buckets[row.id, default: []].insert(row.nextReset ?? "noreset") + } fired = fired.filter { key in let parts = key.split(separator: "|", omittingEmptySubsequences: false).map(String.init) guard let kind = parts.first else { return false } @@ -266,7 +267,7 @@ public enum BarAlertEngine { // parts: [kind, accountId, bucket, "L"]; bucket must equal the // account's CURRENT nextReset and the account must still be present. guard parts.count >= 3, presentIds.contains(parts[1]) else { return false } - return presentResetBuckets[parts[1]] == parts[2] + return presentResetBuckets[parts[1]]?.contains(parts[2]) == true case BarAlertKind.reauthNeeded.rawValue, BarAlertKind.accountCooldownOrPaused.rawValue: // parts: [kind, accountId, "on"]; keep only for still-present accounts. return parts.count >= 2 && presentIds.contains(parts[1])