fix(bar): tolerate duplicate rows in alert pruning (#1553)

Bar alert pruning tolerates duplicate rows without crashing.
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-16 08:45:47 -04:00
1 parent c0409862d2
commit 84ff4d57a4
2 files changed
+33 -3

No files matched your search

+29
View File
@@ -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<String> = [
"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 = [
@@ -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<String>] = 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<level>"]; 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])