974 lines
38 KiB
Swift
974 lines
38 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
@testable import CodexBarWidget
|
|
|
|
struct CodexBarWidgetProviderTests {
|
|
@Test
|
|
func `usage display follows remaining and used preference`() {
|
|
#expect(WidgetUsageDisplay.percent(fromRemaining: 48, showUsed: false) == 48)
|
|
#expect(WidgetUsageDisplay.percent(fromRemaining: 48, showUsed: true) == 52)
|
|
#expect(WidgetUsageDisplay.percent(fromRemaining: nil, showUsed: true) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `small widget falls back to local cost when quota rows are unavailable`() {
|
|
let tokenUsage = WidgetSnapshot.TokenUsageSummary(
|
|
sessionCostUSD: 1.25,
|
|
sessionTokens: 4200,
|
|
last30DaysCostUSD: 12.50,
|
|
last30DaysTokens: 42000,
|
|
currencyCode: "USD",
|
|
sessionLabel: "Today",
|
|
last30DaysLabel: "30d")
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .claude,
|
|
updatedAt: Date(),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: [],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: tokenUsage,
|
|
dailyUsage: [])
|
|
let windowedEntry = WidgetSnapshot.ProviderEntry(
|
|
provider: .claude,
|
|
updatedAt: Date(),
|
|
primary: RateWindow(usedPercent: 25, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: tokenUsage,
|
|
dailyUsage: [])
|
|
|
|
#expect(WidgetUsageRow.compactTokenUsage(for: entry)?.sessionTokens == 4200)
|
|
#expect(WidgetUsageRow.compactTokenUsage(for: windowedEntry) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `small widget limits custom usage rows`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .antigravity,
|
|
updatedAt: Date(),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "one", title: "One", percentLeft: 90),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "two", title: "Two", percentLeft: 80),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "three", title: "Three", percentLeft: 70),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "four", title: "Four", percentLeft: 60),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
#expect(WidgetUsageRow.rows(for: entry, limit: 2).map(\.id) == ["one", "two"])
|
|
#expect(WidgetUsageRow.rows(for: entry).count == 4)
|
|
}
|
|
|
|
@Test
|
|
func `small antigravity widget keeps one row per quota family`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .antigravity,
|
|
updatedAt: Date(),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-gemini-session",
|
|
title: "Gemini Models Five Hour Limit",
|
|
percentLeft: 80),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-gemini-weekly",
|
|
title: "Gemini Models Weekly Limit",
|
|
percentLeft: 20),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-third-party-session",
|
|
title: "Claude and GPT models Five Hour Limit",
|
|
percentLeft: 5),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-third-party-weekly",
|
|
title: "Claude and GPT models Weekly Limit",
|
|
percentLeft: 60),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry, limit: 2)
|
|
|
|
#expect(rows.map(\.title) == ["Gemini Models Weekly Limit", "Claude and GPT models Five Hour Limit"])
|
|
#expect(rows.compactMap(\.percentLeft) == [20, 5])
|
|
#expect(WidgetUsageRow.smallWidgetRowLimit(for: entry) == 2)
|
|
#expect(WidgetUsageRow.mediumWidgetRowLimit(for: entry) == 3)
|
|
let mediumRows = WidgetUsageRow.rows(
|
|
for: entry,
|
|
limit: WidgetUsageRow.mediumWidgetRowLimit(for: entry))
|
|
#expect(mediumRows.map(\.title) == [
|
|
"Gemini Models Weekly Limit",
|
|
"Claude and GPT models Five Hour Limit",
|
|
"Claude and GPT models Weekly Limit",
|
|
])
|
|
}
|
|
|
|
@Test
|
|
func `small antigravity widget keeps claude gpt family when fallback rows are more constrained`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .antigravity,
|
|
updatedAt: Date(),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-gemini-5h",
|
|
title: "Gemini Models Five Hour Limit",
|
|
percentLeft: 40),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-gemini-weekly",
|
|
title: "Gemini Models Weekly Limit",
|
|
percentLeft: 70),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-3p-5h",
|
|
title: "Claude and GPT models Five Hour Limit",
|
|
percentLeft: 60),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-other-5h",
|
|
title: "Other Five Hour Limit",
|
|
percentLeft: 1),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry, limit: 2)
|
|
|
|
#expect(rows.map(\.id) == [
|
|
"antigravity-quota-summary-gemini-5h",
|
|
"antigravity-quota-summary-3p-5h",
|
|
])
|
|
}
|
|
|
|
@Test
|
|
func `small widget preserves tertiary rows for other providers`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .cursor,
|
|
updatedAt: Date(),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "one", title: "One", percentLeft: 90),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "two", title: "Two", percentLeft: 80),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "three", title: "Three", percentLeft: 70),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let limit = WidgetUsageRow.smallWidgetRowLimit(for: entry)
|
|
|
|
#expect(limit == nil)
|
|
#expect(WidgetUsageRow.rows(for: entry, limit: limit).map(\.id) == ["one", "two", "three"])
|
|
}
|
|
|
|
@Test
|
|
func `small antigravity widget prefers known quota rows`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .antigravity,
|
|
updatedAt: Date(),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-gemini-session",
|
|
title: "Gemini Models Five Hour Limit",
|
|
percentLeft: nil),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-gemini-weekly",
|
|
title: "Gemini Models Weekly Limit",
|
|
percentLeft: 100),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-third-party-session",
|
|
title: "Claude and GPT models Five Hour Limit",
|
|
percentLeft: 80),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry, limit: 2)
|
|
|
|
#expect(rows.map(\.title) == ["Gemini Models Weekly Limit", "Claude and GPT models Five Hour Limit"])
|
|
}
|
|
|
|
@Test
|
|
func `small antigravity widget keeps nonstandard quota groups visible`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .antigravity,
|
|
updatedAt: Date(),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-other-session",
|
|
title: "Other Session",
|
|
percentLeft: 70),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "antigravity-quota-summary-other-weekly",
|
|
title: "Other Weekly",
|
|
percentLeft: 40),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry, limit: 2)
|
|
|
|
#expect(rows.map(\.title) == ["Other Weekly", "Other Session"])
|
|
}
|
|
|
|
@Test
|
|
func `provider choice supports alibaba`() {
|
|
#expect(ProviderChoice(provider: .alibaba) == .alibaba)
|
|
#expect(ProviderChoice.alibaba.provider == .alibaba)
|
|
}
|
|
|
|
@Test
|
|
func `provider choice supports alibaba token plan`() {
|
|
#expect(ProviderChoice(provider: .alibabatokenplan) == .alibabatokenplan)
|
|
#expect(ProviderChoice.alibabatokenplan.provider == .alibabatokenplan)
|
|
}
|
|
|
|
@Test
|
|
func `provider choice supports opencode go`() {
|
|
#expect(ProviderChoice(provider: .opencodego) == .opencodego)
|
|
#expect(ProviderChoice.opencodego.provider == .opencodego)
|
|
}
|
|
|
|
@Test
|
|
func `provider choice supports devin`() {
|
|
#expect(ProviderChoice(provider: .devin) == .devin)
|
|
#expect(ProviderChoice.devin.provider == .devin)
|
|
}
|
|
|
|
@Test
|
|
func `widget entry carries devin overage balance through providerCost`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .devin,
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [],
|
|
providerCost: ProviderCostSnapshot(
|
|
used: 48.0,
|
|
limit: 0,
|
|
currencyCode: "USD",
|
|
period: "Extra usage balance",
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000)))
|
|
let encoded = try? JSONEncoder().encode(entry)
|
|
#expect(encoded != nil)
|
|
let decoded = encoded.flatMap { try? JSONDecoder().decode(WidgetSnapshot.ProviderEntry.self, from: $0) }
|
|
#expect(decoded?.providerCost?.period == "Extra usage balance")
|
|
#expect(decoded?.providerCost?.used == 48.0)
|
|
#expect(decoded?.providerCost?.limit == 0)
|
|
#expect(decoded?.provider == .devin)
|
|
}
|
|
|
|
@Test
|
|
func `widget balance formatter renders devin extra usage balance`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .devin,
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [],
|
|
providerCost: ProviderCostSnapshot(
|
|
used: 48.0,
|
|
limit: 0,
|
|
currencyCode: "USD",
|
|
period: "Extra usage balance",
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000)))
|
|
let line = WidgetBalanceFormatter.extraUsageBalance(for: entry)
|
|
#expect(line?.title == "Extra usage")
|
|
#expect(line?.value.hasPrefix("Balance: ") == true)
|
|
#expect(line?.value.contains("48") == true)
|
|
}
|
|
|
|
@Test
|
|
func `compact credits render Devin extra usage balance`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .devin,
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [],
|
|
providerCost: ProviderCostSnapshot(
|
|
used: 48.0,
|
|
limit: 0,
|
|
currencyCode: "USD",
|
|
period: "Extra usage balance",
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000)))
|
|
|
|
let display = CompactMetricFormatter.display(for: entry, metric: .credits)
|
|
|
|
#expect(display.value.contains("48"))
|
|
#expect(display.label == "Extra usage balance")
|
|
#expect(display.detail == nil)
|
|
}
|
|
|
|
@Test
|
|
func `widget balance formatter does not leak another provider balance`() {
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .factory,
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [],
|
|
providerCost: ProviderCostSnapshot(
|
|
used: 12.0,
|
|
limit: 100.0,
|
|
currencyCode: "USD",
|
|
period: "Extra usage balance",
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000)))
|
|
#expect(WidgetBalanceFormatter.extraUsageBalance(for: entry) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `provider choice supports Mistral`() {
|
|
#expect(ProviderChoice(provider: .mistral) == .mistral)
|
|
#expect(ProviderChoice.mistral.provider == .mistral)
|
|
}
|
|
|
|
@Test
|
|
func `provider choice supports Kimi`() {
|
|
#expect(ProviderChoice(provider: .kimi) == .kimi)
|
|
#expect(ProviderChoice.kimi.provider == .kimi)
|
|
}
|
|
|
|
@Test
|
|
func `compact Kimi widgets keep established row fit while large widgets show all quotas`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .kimi,
|
|
updatedAt: now,
|
|
primary: RateWindow(usedPercent: 25, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 50, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "primary", title: "Weekly", percentLeft: 75),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "secondary", title: "Rate Limit", percentLeft: 50),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "kimi-monthly", title: "Monthly", percentLeft: 25),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "kimi-code-7d", title: "Code 7-day", percentLeft: 90),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let smallRows = WidgetUsageRow.rows(
|
|
for: entry,
|
|
limit: WidgetUsageRow.smallWidgetRowLimit(for: entry))
|
|
let mediumRows = WidgetUsageRow.rows(
|
|
for: entry,
|
|
limit: WidgetUsageRow.mediumWidgetRowLimit(for: entry))
|
|
let largeRows = WidgetUsageRow.rows(for: entry)
|
|
|
|
#expect(WidgetUsageRow.smallWidgetRowLimit(for: entry) == 3)
|
|
#expect(WidgetUsageRow.mediumWidgetRowLimit(for: entry) == 3)
|
|
#expect(smallRows.map(\.id) == ["primary", "secondary", "kimi-monthly"])
|
|
#expect(mediumRows == smallRows)
|
|
#expect(largeRows.map(\.id) == ["primary", "secondary", "kimi-monthly", "kimi-code-7d"])
|
|
}
|
|
|
|
@Test
|
|
func `provider choice excludes unsupported Chutes widgets`() {
|
|
#expect(ProviderChoice(provider: .chutes) == nil)
|
|
#expect(ProviderChoice(provider: .sub2api) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `supported providers fall back to codex when snapshot is empty`() {
|
|
let snapshot = WidgetSnapshot(entries: [], enabledProviders: [], generatedAt: Date())
|
|
|
|
#expect(CodexBarSwitcherTimelineProvider.supportedProviders(from: snapshot) == [.codex])
|
|
}
|
|
|
|
@Test
|
|
func `supported providers keep alibaba when it is the only enabled provider`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .alibaba,
|
|
updatedAt: now,
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
let snapshot = WidgetSnapshot(entries: [entry], enabledProviders: [.alibaba], generatedAt: now)
|
|
|
|
#expect(CodexBarSwitcherTimelineProvider.supportedProviders(from: snapshot) == [.alibaba])
|
|
}
|
|
|
|
@Test
|
|
func `supported providers keep alibaba token plan when it is the only enabled provider`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .alibabatokenplan,
|
|
updatedAt: now,
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
let snapshot = WidgetSnapshot(entries: [entry], enabledProviders: [.alibabatokenplan], generatedAt: now)
|
|
|
|
#expect(CodexBarSwitcherTimelineProvider.supportedProviders(from: snapshot) == [.alibabatokenplan])
|
|
}
|
|
|
|
@Test
|
|
func `supported providers keep Mistral when it is the only enabled provider`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .mistral,
|
|
updatedAt: now,
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
let snapshot = WidgetSnapshot(entries: [entry], enabledProviders: [.mistral], generatedAt: now)
|
|
|
|
#expect(CodexBarSwitcherTimelineProvider.supportedProviders(from: snapshot) == [.mistral])
|
|
}
|
|
|
|
@Test
|
|
func `supported providers keep Kimi when it is the only enabled provider`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .kimi,
|
|
updatedAt: now,
|
|
primary: RateWindow(usedPercent: 25, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 50, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
let snapshot = WidgetSnapshot(entries: [entry], enabledProviders: [.kimi], generatedAt: now)
|
|
|
|
#expect(CodexBarSwitcherTimelineProvider.supportedProviders(from: snapshot) == [.kimi])
|
|
}
|
|
|
|
@Test
|
|
func `codex weekly only widget rows omit session`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .codex,
|
|
updatedAt: now,
|
|
primary: nil,
|
|
secondary: RateWindow(usedPercent: 25, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry)
|
|
|
|
#expect(rows.count == 1)
|
|
#expect(rows.first?.title == "Weekly")
|
|
#expect(rows.first?.percentLeft == 75)
|
|
}
|
|
|
|
@Test
|
|
func `codex widget usage rows keep code review separate from rate rows`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .codex,
|
|
updatedAt: now,
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 25, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: 60,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry)
|
|
|
|
#expect(rows.map(\.title) == ["Session", "Weekly"])
|
|
#expect(rows.count == 2)
|
|
#expect(!rows.contains { $0.title == "Code review" })
|
|
}
|
|
|
|
@Test
|
|
func `widget usage rows prefer projected rows over legacy slots`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .codex,
|
|
updatedAt: now,
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 25, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(id: "weekly", title: "Weekly", percentLeft: 75),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry)
|
|
|
|
#expect(rows == [WidgetUsageRow(id: "weekly", title: "Weekly", percentLeft: 75)])
|
|
}
|
|
|
|
@Test
|
|
func `codex widget session cap lifts at weekly reset without a new snapshot`() {
|
|
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let weeklyReset = now.addingTimeInterval(3600)
|
|
let sessionWindow = RateWindow(
|
|
usedPercent: 1,
|
|
windowMinutes: 300,
|
|
resetsAt: now.addingTimeInterval(1800),
|
|
resetDescription: nil)
|
|
let weeklyWindow = RateWindow(
|
|
usedPercent: 100,
|
|
windowMinutes: 10080,
|
|
resetsAt: weeklyReset,
|
|
resetDescription: nil)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .codex,
|
|
updatedAt: now.addingTimeInterval(-7200),
|
|
primary: sessionWindow,
|
|
secondary: weeklyWindow,
|
|
tertiary: nil,
|
|
usageRows: [
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "session",
|
|
title: "Session",
|
|
percentLeft: 99,
|
|
window: sessionWindow),
|
|
WidgetSnapshot.WidgetUsageRowSnapshot(
|
|
id: "weekly",
|
|
title: "Weekly",
|
|
percentLeft: 0,
|
|
window: weeklyWindow),
|
|
],
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let capped = WidgetUsageRow.rows(for: entry, now: now)
|
|
let reset = WidgetUsageRow.rows(for: entry, now: weeklyReset)
|
|
|
|
#expect(capped.map(\.percentLeft) == [0, 0])
|
|
#expect(reset.map(\.percentLeft) == [99, 0])
|
|
}
|
|
|
|
@Test
|
|
func `legacy widget usage rows use antigravity grouped slots`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .antigravity,
|
|
updatedAt: now,
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
tertiary: RateWindow(usedPercent: 30, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
|
|
let rows = WidgetUsageRow.rows(for: entry)
|
|
|
|
#expect(rows.map(\.id) == ["primary", "secondary"])
|
|
#expect(rows.map(\.title) == ["Gemini Models", "Claude and GPT"])
|
|
#expect(rows.compactMap(\.percentLeft) == [90, 80])
|
|
}
|
|
|
|
@Test
|
|
func `widget configuration intents default to codex and credits`() {
|
|
let providerIntent = ProviderSelectionIntent()
|
|
let compactIntent = CompactMetricSelectionIntent()
|
|
let burnIntent = BurnDownSelectionIntent()
|
|
let combinedBurnIntent = BurnProviderSelectionIntent()
|
|
|
|
#expect(providerIntent.provider == .codex)
|
|
#expect(compactIntent.provider == .codex)
|
|
#expect(compactIntent.metric == .credits)
|
|
#expect(burnIntent.provider == .codex)
|
|
#expect(burnIntent.window == .session)
|
|
#expect(combinedBurnIntent.provider == .codex)
|
|
}
|
|
|
|
@Test
|
|
func `burn down uses an exact provider entry`() {
|
|
let snapshot = Self.burnSnapshot(provider: .claude, primaryUsed: 20, secondaryUsed: 30)
|
|
|
|
#expect(BurnDownState(snapshot: snapshot, provider: .codex, selection: .session) == nil)
|
|
#expect(BurnDownState(snapshot: snapshot, provider: .claude, selection: .session) != nil)
|
|
}
|
|
|
|
@Test
|
|
func `codex exhausted weekly cap blocks the session chart until weekly reset`() throws {
|
|
let weeklyReset = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let snapshot = Self.burnSnapshot(
|
|
provider: .codex,
|
|
primaryUsed: 80,
|
|
secondaryUsed: 100,
|
|
primaryReset: weeklyReset.addingTimeInterval(-3600),
|
|
secondaryReset: weeklyReset)
|
|
let state = try #require(BurnDownState(snapshot: snapshot, provider: .codex, selection: .session))
|
|
|
|
#expect(state.secondaryGloballyCapsPrimary)
|
|
#expect(state.primaryWindow?.remainingPercent == 0)
|
|
#expect(state.blankPrimaryChart)
|
|
#expect(state.selectedResetOverride == weeklyReset)
|
|
}
|
|
|
|
@Test
|
|
func `gemini exhausted secondary window does not block the independent primary`() throws {
|
|
let primaryReset = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let snapshot = Self.burnSnapshot(
|
|
provider: .gemini,
|
|
primaryUsed: 20,
|
|
secondaryUsed: 100,
|
|
primaryReset: primaryReset,
|
|
secondaryReset: primaryReset.addingTimeInterval(-3600))
|
|
let state = try #require(BurnDownState(snapshot: snapshot, provider: .gemini, selection: .session))
|
|
|
|
#expect(!state.secondaryGloballyCapsPrimary)
|
|
#expect(state.primaryWindow?.remainingPercent == 80)
|
|
#expect(!state.blankPrimaryChart)
|
|
#expect(state.selectedResetOverride == nil)
|
|
}
|
|
|
|
@Test
|
|
func `independent secondary reset never overrides primary reset`() throws {
|
|
let primaryReset = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let snapshot = Self.burnSnapshot(
|
|
provider: .gemini,
|
|
primaryUsed: 20,
|
|
secondaryUsed: 30,
|
|
primaryReset: primaryReset,
|
|
secondaryReset: primaryReset.addingTimeInterval(-3600))
|
|
let state = try #require(BurnDownState(snapshot: snapshot, provider: .gemini, selection: .session))
|
|
|
|
#expect(state.selectedWindow?.resetsAt == primaryReset)
|
|
#expect(state.selectedResetOverride == nil)
|
|
}
|
|
|
|
@Test
|
|
func `burn down preview includes session and weekly windows`() throws {
|
|
let snapshot = WidgetPreviewData.snapshot()
|
|
|
|
let session = try #require(BurnDownState(snapshot: snapshot, provider: .codex, selection: .session))
|
|
let weekly = try #require(BurnDownState(snapshot: snapshot, provider: .codex, selection: .weekly))
|
|
|
|
#expect(session.selectedWindow?.windowMinutes == 300)
|
|
#expect(weekly.selectedWindow?.windowMinutes == 10080)
|
|
}
|
|
|
|
@Test
|
|
func `burn down selection does not fall back to another window`() throws {
|
|
let weeklyOnly = Self.burnSnapshot(provider: .codex, primaryUsed: nil, secondaryUsed: 30)
|
|
let sessionOnly = Self.burnSnapshot(provider: .codex, primaryUsed: 20, secondaryUsed: nil)
|
|
let weeklyStoredInPrimary = Self.burnSnapshot(
|
|
provider: .claude,
|
|
primaryUsed: 30,
|
|
secondaryUsed: nil,
|
|
primaryWindowMinutes: 7 * 24 * 60)
|
|
|
|
let weeklyOnlySession = try #require(BurnDownState(
|
|
snapshot: weeklyOnly,
|
|
provider: .codex,
|
|
selection: .session))
|
|
let weeklyOnlyWeekly = try #require(BurnDownState(
|
|
snapshot: weeklyOnly,
|
|
provider: .codex,
|
|
selection: .weekly))
|
|
let sessionOnlySession = try #require(BurnDownState(
|
|
snapshot: sessionOnly,
|
|
provider: .codex,
|
|
selection: .session))
|
|
let sessionOnlyWeekly = try #require(BurnDownState(
|
|
snapshot: sessionOnly,
|
|
provider: .codex,
|
|
selection: .weekly))
|
|
let weeklyPrimarySession = try #require(BurnDownState(
|
|
snapshot: weeklyStoredInPrimary,
|
|
provider: .claude,
|
|
selection: .session))
|
|
let weeklyPrimaryWeekly = try #require(BurnDownState(
|
|
snapshot: weeklyStoredInPrimary,
|
|
provider: .claude,
|
|
selection: .weekly))
|
|
|
|
#expect(weeklyOnlySession.selectedWindow == nil)
|
|
#expect(weeklyOnlyWeekly.selectedWindow == weeklyOnlyWeekly.secondaryWindow)
|
|
#expect(sessionOnlySession.selectedWindow == sessionOnlySession.primaryWindow)
|
|
#expect(sessionOnlyWeekly.selectedWindow == nil)
|
|
#expect(weeklyPrimarySession.selectedWindow == nil)
|
|
#expect(weeklyPrimaryWeekly.selectedWindow?.usedPercent == 30)
|
|
}
|
|
|
|
@Test
|
|
func `expired weekly reset no longer blocks the session chart`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let snapshot = Self.burnSnapshot(
|
|
provider: .codex,
|
|
primaryUsed: 20,
|
|
secondaryUsed: 100,
|
|
primaryReset: now.addingTimeInterval(300),
|
|
secondaryReset: now.addingTimeInterval(-1))
|
|
let state = try #require(BurnDownState(
|
|
snapshot: snapshot,
|
|
provider: .codex,
|
|
selection: .session,
|
|
now: now))
|
|
|
|
#expect(!state.secondaryExhausted)
|
|
#expect(state.primaryWindow?.remainingPercent == 80)
|
|
#expect(!state.blankPrimaryChart)
|
|
#expect(state.selectedResetOverride == nil)
|
|
}
|
|
|
|
@Test
|
|
func `explicit reset takes precedence over estimated reset`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let future = now.addingTimeInterval(600)
|
|
|
|
#expect(burnEffectiveResetDate(
|
|
explicitResetAt: now.addingTimeInterval(-1),
|
|
estimatedResetMinutes: 5,
|
|
now: now) == nil)
|
|
#expect(burnEffectiveResetDate(
|
|
explicitResetAt: future,
|
|
estimatedResetMinutes: 5,
|
|
now: now) == future)
|
|
#expect(burnEffectiveResetDate(
|
|
explicitResetAt: nil,
|
|
estimatedResetMinutes: 5,
|
|
now: now) == now.addingTimeInterval(300))
|
|
#expect(burnEffectiveResetDate(
|
|
explicitResetAt: nil,
|
|
estimatedResetMinutes: nil,
|
|
now: now) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `burn down axis shares the effective estimated reset`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let effectiveReset = try #require(burnEffectiveResetDate(
|
|
explicitResetAt: nil,
|
|
estimatedResetMinutes: 90,
|
|
now: now))
|
|
|
|
let axis = burnAxisDateRange(
|
|
effectiveResetAt: effectiveReset,
|
|
windowMinutes: 300,
|
|
now: now)
|
|
|
|
#expect(axis.reset == effectiveReset)
|
|
#expect(axis.start == effectiveReset.addingTimeInterval(-5 * 60 * 60))
|
|
}
|
|
|
|
@Test
|
|
func `burn down refreshes immediately after the earliest future reset`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let snapshot = Self.burnSnapshot(
|
|
provider: .codex,
|
|
primaryUsed: 20,
|
|
secondaryUsed: 30,
|
|
primaryReset: now.addingTimeInterval(60),
|
|
secondaryReset: now.addingTimeInterval(120))
|
|
|
|
#expect(BurnDownRefreshSchedule.nextRefresh(snapshot: snapshot, provider: .codex, now: now)
|
|
== now.addingTimeInterval(61))
|
|
}
|
|
|
|
@Test
|
|
func `burn down refresh ignores past resets and unrelated provider entries`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let snapshot = Self.burnSnapshot(
|
|
provider: .claude,
|
|
primaryUsed: 20,
|
|
secondaryUsed: 30,
|
|
primaryReset: now.addingTimeInterval(-60),
|
|
secondaryReset: now.addingTimeInterval(-30))
|
|
let fallback = now.addingTimeInterval(30 * 60)
|
|
|
|
#expect(BurnDownRefreshSchedule.nextRefresh(snapshot: snapshot, provider: .claude, now: now) == fallback)
|
|
#expect(BurnDownRefreshSchedule.nextRefresh(snapshot: snapshot, provider: .codex, now: now) == fallback)
|
|
}
|
|
|
|
private static func burnSnapshot(
|
|
provider: UsageProvider,
|
|
primaryUsed: Double?,
|
|
secondaryUsed: Double?,
|
|
primaryReset: Date? = nil,
|
|
secondaryReset: Date? = nil,
|
|
primaryWindowMinutes: Int = 5 * 60,
|
|
secondaryWindowMinutes: Int = 7 * 24 * 60) -> WidgetSnapshot
|
|
{
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: provider,
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
primary: primaryUsed.map {
|
|
RateWindow(
|
|
usedPercent: $0,
|
|
windowMinutes: primaryWindowMinutes,
|
|
resetsAt: primaryReset,
|
|
resetDescription: nil)
|
|
},
|
|
secondary: secondaryUsed.map {
|
|
RateWindow(
|
|
usedPercent: $0,
|
|
windowMinutes: secondaryWindowMinutes,
|
|
resetsAt: secondaryReset,
|
|
resetDescription: nil)
|
|
},
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
return WidgetSnapshot(entries: [entry], generatedAt: entry.updatedAt)
|
|
}
|
|
}
|
|
|
|
extension CodexBarWidgetProviderTests {
|
|
@Test
|
|
func `provider choice supports Cursor`() {
|
|
#expect(ProviderChoice(provider: .cursor) == .cursor)
|
|
#expect(ProviderChoice.cursor.provider == .cursor)
|
|
}
|
|
|
|
@Test
|
|
func `supported providers keep Cursor when it is the only enabled provider`() {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .cursor,
|
|
updatedAt: now,
|
|
primary: RateWindow(usedPercent: 25, windowMinutes: 43200, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: nil,
|
|
dailyUsage: [])
|
|
let snapshot = WidgetSnapshot(entries: [entry], enabledProviders: [.cursor], generatedAt: now)
|
|
|
|
#expect(CodexBarSwitcherTimelineProvider.supportedProviders(from: snapshot) == [.cursor])
|
|
}
|
|
|
|
@Test
|
|
func `widget token titles disclose stale age for today and history rows`() {
|
|
let entryUpdatedAt = Date()
|
|
let staleToken = WidgetSnapshot.TokenUsageSummary(
|
|
sessionCostUSD: 1.25,
|
|
sessionTokens: 4200,
|
|
last30DaysCostUSD: 12.50,
|
|
last30DaysTokens: 42000,
|
|
updatedAt: entryUpdatedAt.addingTimeInterval(-45 * 60))
|
|
let freshToken = WidgetSnapshot.TokenUsageSummary(
|
|
sessionCostUSD: 1.25,
|
|
sessionTokens: 4200,
|
|
last30DaysCostUSD: 12.50,
|
|
last30DaysTokens: 42000,
|
|
updatedAt: entryUpdatedAt.addingTimeInterval(-5 * 60))
|
|
|
|
let todayTitle = WidgetFormat.tokenRowTitle(
|
|
staleToken.sessionLabel,
|
|
summary: staleToken,
|
|
entryUpdatedAt: entryUpdatedAt)
|
|
let historyTitle = WidgetFormat.tokenRowTitle(
|
|
staleToken.last30DaysLabel,
|
|
summary: staleToken,
|
|
entryUpdatedAt: entryUpdatedAt)
|
|
|
|
#expect(todayTitle.hasPrefix("Today · "))
|
|
#expect(historyTitle.hasPrefix("30d · "))
|
|
#expect(WidgetFormat.tokenRowTitle(
|
|
freshToken.sessionLabel,
|
|
summary: freshToken,
|
|
entryUpdatedAt: entryUpdatedAt) == "Today")
|
|
|
|
let entry = WidgetSnapshot.ProviderEntry(
|
|
provider: .codex,
|
|
updatedAt: entryUpdatedAt,
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
creditsRemaining: nil,
|
|
codeReviewRemainingPercent: nil,
|
|
tokenUsage: staleToken,
|
|
dailyUsage: [])
|
|
let todayMetric = CompactMetricFormatter.display(for: entry, metric: .todayCost)
|
|
let historyMetric = CompactMetricFormatter.display(for: entry, metric: .last30DaysCost)
|
|
|
|
#expect(todayMetric.label.hasPrefix("Today cost · "))
|
|
#expect(historyMetric.label.hasPrefix("30d cost · "))
|
|
}
|
|
|
|
@Test
|
|
func `usage history chart mode requires every point to expose cost`() {
|
|
let costPoints = [
|
|
WidgetSnapshot.DailyUsagePoint(dayKey: "2026-07-01", totalTokens: 100, costUSD: 1.2),
|
|
WidgetSnapshot.DailyUsagePoint(dayKey: "2026-07-02", totalTokens: 200, costUSD: 2.4),
|
|
]
|
|
let tokenPoints = [
|
|
WidgetSnapshot.DailyUsagePoint(dayKey: "2026-07-01", totalTokens: 100, costUSD: nil),
|
|
WidgetSnapshot.DailyUsagePoint(dayKey: "2026-07-02", totalTokens: 200, costUSD: nil),
|
|
]
|
|
let mixedPoints = [
|
|
WidgetSnapshot.DailyUsagePoint(dayKey: "2026-07-01", totalTokens: 100, costUSD: 1.2),
|
|
WidgetSnapshot.DailyUsagePoint(dayKey: "2026-07-02", totalTokens: 200, costUSD: nil),
|
|
]
|
|
let emptyPoints: [WidgetSnapshot.DailyUsagePoint] = []
|
|
|
|
#expect(UsageHistoryChartMode.isCostMode(costPoints) == true)
|
|
#expect(UsageHistoryChartMode.isCostMode(tokenPoints) == false)
|
|
#expect(UsageHistoryChartMode.isCostMode(mixedPoints) == false)
|
|
#expect(UsageHistoryChartMode.isCostMode(emptyPoints) == false)
|
|
}
|
|
}
|