diff --git a/Sources/CodexBarCore/Providers/JetBrains/JetBrainsStatusProbe.swift b/Sources/CodexBarCore/Providers/JetBrains/JetBrainsStatusProbe.swift index 198d830d1a..4799a667c8 100644 --- a/Sources/CodexBarCore/Providers/JetBrains/JetBrainsStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/JetBrains/JetBrainsStatusProbe.swift @@ -81,16 +81,15 @@ public struct JetBrainsStatusSnapshot: Sendable { identity: identity) } - private static func formatResetDescription(_ date: Date?) -> String? { + static func formatResetDescription(_ date: Date?, now: Date = Date()) -> String? { guard let date else { return nil } - let now = Date() let interval = date.timeIntervalSince(now) guard interval > 0 else { return "Expired" } let hours = Int(interval / 3600) let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60) - if hours > 24 { + if hours >= 24 { let days = hours / 24 let remainingHours = hours % 24 return "Resets in \(days)d \(remainingHours)h" diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift index 73f863ec8e..09a87594d5 100644 --- a/Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift @@ -259,16 +259,15 @@ extension WindsurfCachedPlanInfo { resetDescription: "\(clampedUsed) / \(total) \(unit)") } - private static func formatResetDescription(_ date: Date?) -> String? { + static func formatResetDescription(_ date: Date?, now: Date = Date()) -> String? { guard let date else { return nil } - let now = Date() let interval = date.timeIntervalSince(now) guard interval > 0 else { return "Expired" } let hours = Int(interval / 3600) let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60) - if hours > 24 { + if hours >= 24 { let days = hours / 24 let remainingHours = hours % 24 return "Resets in \(days)d \(remainingHours)h" diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift index 27bc8579f0..b2c3ddf8b1 100644 --- a/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift +++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift @@ -79,16 +79,15 @@ extension WindsurfGetPlanStatusResponse { identity: identity) } - private static func formatResetDescription(_ date: Date?) -> String? { + static func formatResetDescription(_ date: Date?, now: Date = Date()) -> String? { guard let date else { return nil } - let now = Date() let interval = date.timeIntervalSince(now) guard interval > 0 else { return "Expired" } let hours = Int(interval / 3600) let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60) - if hours > 24 { + if hours >= 24 { let days = hours / 24 let remainingHours = hours % 24 return "Resets in \(days)d \(remainingHours)h" diff --git a/Sources/CodexBarCore/Providers/Zed/ZedStatusProbe.swift b/Sources/CodexBarCore/Providers/Zed/ZedStatusProbe.swift index dddacef7bf..f082d1bb34 100644 --- a/Sources/CodexBarCore/Providers/Zed/ZedStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Zed/ZedStatusProbe.swift @@ -527,15 +527,14 @@ extension ZedUsageSnapshot { return max(0, min(100, elapsed / total * 100)) } - private static func formatResetDescription(_ date: Date) -> String? { - let now = Date() + static func formatResetDescription(_ date: Date, now: Date = Date()) -> String? { let interval = date.timeIntervalSince(now) guard interval > 0 else { return "Cycle ended" } let hours = Int(interval / 3600) let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60) - if hours > 24 { + if hours >= 24 { let days = hours / 24 let remainingHours = hours % 24 return "Cycle ends in \(days)d \(remainingHours)h" diff --git a/Tests/CodexBarTests/ResetCountdownDayRolloverTests.swift b/Tests/CodexBarTests/ResetCountdownDayRolloverTests.swift new file mode 100644 index 0000000000..e0c9a796e3 --- /dev/null +++ b/Tests/CodexBarTests/ResetCountdownDayRolloverTests.swift @@ -0,0 +1,55 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct ResetCountdownDayRolloverTests { + private static let now = Date(timeIntervalSince1970: 1_700_000_000) + + private func at(hoursFromNow hours: Double) -> Date { + Self.now.addingTimeInterval(hours * 3600) + } + + @Test + func `Windsurf web reset at exactly 24h rolls over to a day`() { + // Was "Resets in 24h 0m"; the day form must be reachable at the 24h boundary. + #expect( + WindsurfGetPlanStatusResponse.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Resets in 1d 0h") + } + + @Test + func `Windsurf web reset above 24h shows day and hour`() { + #expect( + WindsurfGetPlanStatusResponse.formatResetDescription(self.at(hoursFromNow: 25), now: Self.now) + == "Resets in 1d 1h") + } + + @Test + func `Windsurf web reset below 24h stays in hours`() { + #expect( + WindsurfGetPlanStatusResponse.formatResetDescription(self.at(hoursFromNow: 23), now: Self.now) + == "Resets in 23h 0m") + } + + @Test + func `Windsurf cached reset at exactly 24h rolls over to a day`() { + #expect( + WindsurfCachedPlanInfo.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Resets in 1d 0h") + } + + @Test + func `Zed cycle at exactly 24h rolls over to a day`() { + // Was "Cycle ends in 24h 0m". + #expect( + ZedUsageSnapshot.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Cycle ends in 1d 0h") + } + + @Test + func `JetBrains reset at exactly 24h rolls over to a day`() { + #expect( + JetBrainsStatusSnapshot.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Resets in 1d 0h") + } +} diff --git a/TestsLinux/ResetCountdownDayRolloverLinuxTests.swift b/TestsLinux/ResetCountdownDayRolloverLinuxTests.swift new file mode 100644 index 0000000000..474ca208d5 --- /dev/null +++ b/TestsLinux/ResetCountdownDayRolloverLinuxTests.swift @@ -0,0 +1,57 @@ +#if os(Linux) +import Foundation +import Testing +@testable import CodexBarCore + +struct ResetCountdownDayRolloverLinuxTests { + private static let now = Date(timeIntervalSince1970: 1_700_000_000) + + private func at(hoursFromNow hours: Double) -> Date { + Self.now.addingTimeInterval(hours * 3600) + } + + @Test + func `Windsurf web reset at exactly 24h rolls over to a day`() { + // Was "Resets in 24h 0m"; the day form must be reachable at the 24h boundary. + #expect( + WindsurfGetPlanStatusResponse.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Resets in 1d 0h") + } + + @Test + func `Windsurf web reset above 24h shows day and hour`() { + #expect( + WindsurfGetPlanStatusResponse.formatResetDescription(self.at(hoursFromNow: 25), now: Self.now) + == "Resets in 1d 1h") + } + + @Test + func `Windsurf web reset below 24h stays in hours`() { + #expect( + WindsurfGetPlanStatusResponse.formatResetDescription(self.at(hoursFromNow: 23), now: Self.now) + == "Resets in 23h 0m") + } + + @Test + func `Windsurf cached reset at exactly 24h rolls over to a day`() { + #expect( + WindsurfCachedPlanInfo.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Resets in 1d 0h") + } + + @Test + func `Zed cycle at exactly 24h rolls over to a day`() { + // Was "Cycle ends in 24h 0m". + #expect( + ZedUsageSnapshot.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Cycle ends in 1d 0h") + } + + @Test + func `JetBrains reset at exactly 24h rolls over to a day`() { + #expect( + JetBrainsStatusSnapshot.formatResetDescription(self.at(hoursFromNow: 24), now: Self.now) + == "Resets in 1d 0h") + } +} +#endif