1192 lines
43 KiB
Swift
1192 lines
43 KiB
Swift
import CodexBarCore
|
||
import Foundation
|
||
import Testing
|
||
@testable import CodexBarCLI
|
||
|
||
// swiftlint:disable:next type_body_length
|
||
struct CLISnapshotTests {
|
||
@Test
|
||
func `renders Gemini paid plan without changing acronym casing`() {
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .gemini,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: "Gemini Code Assist in Google One AI Pro")
|
||
let snapshot = UsageSnapshot(
|
||
primary: nil,
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0),
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .gemini,
|
||
snapshot: snapshot,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Gemini",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(CLIRenderer.planBadgeText(provider: .gemini, snapshot: snapshot) ==
|
||
"Gemini Code Assist in Google One AI Pro")
|
||
#expect(output.contains("Plan: Gemini Code Assist in Google One AI Pro"))
|
||
#expect(!output.contains("Google One Ai Pro"))
|
||
}
|
||
|
||
@Test
|
||
func `renders Factory token rate billing with time window labels`() {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: .init(usedPercent: 25, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
||
tertiary: .init(usedPercent: 50, windowMinutes: 43200, resetsAt: nil, resetDescription: nil),
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .factory,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Droid (factory)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("5-hour: 88% left"))
|
||
#expect(output.contains("Weekly: 75% left"))
|
||
#expect(output.contains("Monthly: 50% left"))
|
||
#expect(!output.contains("Standard:"))
|
||
#expect(!output.contains("Premium:"))
|
||
}
|
||
|
||
@Test
|
||
func `renders Factory legacy billing with pool labels`() {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 12, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
||
secondary: .init(usedPercent: 25, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .factory,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Droid (factory)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Standard: 88% left"))
|
||
#expect(output.contains("Premium: 75% left"))
|
||
#expect(!output.contains("5-hour:"))
|
||
#expect(!output.contains("Monthly:"))
|
||
}
|
||
|
||
@Test
|
||
func `renders text snapshot for codex`() {
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .codex,
|
||
accountEmail: "user@example.com",
|
||
accountOrganization: nil,
|
||
loginMethod: "pro")
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: "today at 3:00 PM"),
|
||
secondary: .init(usedPercent: 25, windowMinutes: 10080, resetsAt: nil, resetDescription: "Fri at 9:00 AM"),
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0),
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: CreditsSnapshot(remaining: 42, events: [], updatedAt: Date()),
|
||
context: RenderContext(
|
||
header: "Codex 1.2.3 (codex-cli)",
|
||
status: ProviderStatusPayload(
|
||
indicator: .minor,
|
||
description: "Degraded performance",
|
||
updatedAt: Date(timeIntervalSince1970: 0),
|
||
url: "https://status.example.com"),
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Codex 1.2.3 (codex-cli)"))
|
||
#expect(output.contains("Status: Partial outage – Degraded performance"))
|
||
#expect(output.contains("Codex"))
|
||
#expect(output.contains("Session: 88% left"))
|
||
#expect(output.contains("Weekly: 75% left"))
|
||
#expect(output.contains("Credits: 42"))
|
||
#expect(output.contains("Account: user@example.com"))
|
||
#expect(output.contains("Plan: Pro 20x"))
|
||
}
|
||
|
||
@Test
|
||
func `renders Codex limit reset credits`() {
|
||
let now = Date()
|
||
let expiresAt = now.addingTimeInterval(7200)
|
||
let resetCredits = CodexRateLimitResetCreditsSnapshot(
|
||
credits: [
|
||
CodexRateLimitResetCredit(
|
||
id: "credit-1",
|
||
resetType: "codex_rate_limits",
|
||
status: .available,
|
||
grantedAt: Date(timeIntervalSince1970: 0),
|
||
expiresAt: expiresAt,
|
||
redeemStartedAt: nil,
|
||
redeemedAt: nil,
|
||
title: nil,
|
||
description: nil),
|
||
CodexRateLimitResetCredit(
|
||
id: "expired-credit",
|
||
resetType: "codex_rate_limits",
|
||
status: .available,
|
||
grantedAt: Date(timeIntervalSince1970: 0),
|
||
expiresAt: now,
|
||
redeemStartedAt: nil,
|
||
redeemedAt: nil,
|
||
title: nil,
|
||
description: nil),
|
||
],
|
||
availableCount: 99,
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
let snapshot = UsageSnapshot(
|
||
primary: .init(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
codexResetCredits: resetCredits,
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snapshot,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex (oauth)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown))
|
||
|
||
#expect(output.contains("Limit Reset Credits: 1 available"))
|
||
#expect(output.contains("Next reset credit expires"))
|
||
}
|
||
|
||
@Test
|
||
func `renders Codex prolite plan with multiplier display name`() {
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .codex,
|
||
accountEmail: "user@example.com",
|
||
accountOrganization: nil,
|
||
loginMethod: "prolite")
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: "today at 3:00 PM"),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0),
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 1.2.3 (codex-cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Plan: Pro 5x"))
|
||
#expect(!output.contains("Plan: Pro Lite"))
|
||
#expect(!output.contains("Plan: Prolite"))
|
||
}
|
||
|
||
@Test
|
||
func `renders Codex plan only limits as unavailable`() {
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .codex,
|
||
accountEmail: "user@example.com",
|
||
accountOrganization: nil,
|
||
loginMethod: "pro")
|
||
let snap = UsageSnapshot(
|
||
primary: nil,
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0),
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 1.2.3 (codex-cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Limits: not available"))
|
||
#expect(output.contains("Account: user@example.com"))
|
||
#expect(output.contains("Plan: Pro 20x"))
|
||
#expect(!output.contains("Session:"))
|
||
#expect(!output.contains("Weekly:"))
|
||
}
|
||
|
||
@Test
|
||
func `renders text snapshot for claude without weekly`() {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 2, windowMinutes: nil, resetsAt: nil, resetDescription: "3pm (Europe/Vienna)"),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .claude,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Claude Code 2.0.69 (claude)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Session: 98% left"))
|
||
#expect(!output.contains("Weekly:"))
|
||
}
|
||
|
||
@Test
|
||
func `renders Claude Max multiplier without uppercasing x`() {
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .claude,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: "Claude Max 5x")
|
||
let snapshot = UsageSnapshot(
|
||
primary: .init(usedPercent: 2, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0),
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .claude,
|
||
snapshot: snapshot,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Claude (oauth)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Plan: Claude Max 5x"))
|
||
#expect(!output.contains("Plan: Claude Max 5X"))
|
||
}
|
||
|
||
@Test
|
||
func `renders warp unlimited as detail not reset`() {
|
||
let meta = ProviderDescriptorRegistry.descriptor(for: .warp).metadata
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: "Unlimited"),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0),
|
||
identity: ProviderIdentitySnapshot(
|
||
providerID: .warp,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: nil))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .warp,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Warp 0.0.0 (warp)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("\(meta.sessionLabel): 100% left"))
|
||
#expect(!output.contains("Resets Unlimited"))
|
||
#expect(output.contains("Unlimited"))
|
||
}
|
||
|
||
@Test
|
||
func `renders warp credits as detail and reset as date`() {
|
||
let meta = ProviderDescriptorRegistry.descriptor(for: .warp).metadata
|
||
let now = Date(timeIntervalSince1970: 0)
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 10,
|
||
windowMinutes: nil,
|
||
resetsAt: now.addingTimeInterval(3600),
|
||
resetDescription: "10/100 credits"),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now,
|
||
identity: ProviderIdentitySnapshot(
|
||
providerID: .warp,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: nil))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .warp,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Warp 0.0.0 (warp)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("\(meta.sessionLabel): 90% left"))
|
||
#expect(output.contains("Resets"))
|
||
#expect(output.contains("10/100 credits"))
|
||
#expect(!output.contains("Resets 10/100 credits"))
|
||
}
|
||
|
||
@Test
|
||
func `renders crof dollar balance as detail not reset`() {
|
||
let meta = ProviderDescriptorRegistry.descriptor(for: .crof).metadata
|
||
let snap = CrofUsageSnapshot(
|
||
credits: 9.9999,
|
||
requestsPlan: 1000,
|
||
usableRequests: 998,
|
||
updatedAt: Date(timeIntervalSince1970: 0)).toUsageSnapshot()
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .crof,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Crof",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown))
|
||
|
||
#expect(output.contains("\(meta.sessionLabel): 99% left"))
|
||
#expect(output.contains("\(meta.weeklyLabel): 100% left"))
|
||
#expect(output.contains("$9.99"))
|
||
#expect(!output.contains("Resets $9.99"))
|
||
}
|
||
|
||
@Test
|
||
func `renders qoder reset and credit total separately`() {
|
||
let meta = ProviderDescriptorRegistry.descriptor(for: .qoder).metadata
|
||
let now = Date(timeIntervalSince1970: 0)
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 25,
|
||
windowMinutes: nil,
|
||
resetsAt: now.addingTimeInterval(3600),
|
||
resetDescription: "125 / 500 credits"),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now,
|
||
identity: ProviderIdentitySnapshot(
|
||
providerID: .qoder,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: nil))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .qoder,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Qoder",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown),
|
||
now: now)
|
||
|
||
#expect(output.contains("\(meta.sessionLabel): 75% left"))
|
||
#expect(output.contains("Resets in 1h"))
|
||
#expect(output.contains("125 / 500 credits"))
|
||
#expect(!output.contains("Resets 125 / 500 credits"))
|
||
}
|
||
|
||
@Test
|
||
func `renders kilo plan activity and fallback note`() {
|
||
let now = Date(timeIntervalSince1970: 0)
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .kilo,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: "Kilo Pass Pro · Auto top-up: visa")
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "40/100 credits"),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now,
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .kilo,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Kilo (cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute,
|
||
notes: ["Using CLI fallback"]))
|
||
|
||
#expect(output.contains("Credits: 60% left"))
|
||
#expect(output.contains("40/100 credits"))
|
||
#expect(!output.contains("Resets 40/100 credits"))
|
||
#expect(output.contains("Plan: Kilo Pass Pro"))
|
||
#expect(output.contains("Activity: Auto top-up: visa"))
|
||
#expect(output.contains("Note: Using CLI fallback"))
|
||
}
|
||
|
||
@Test
|
||
func `renders kilo zero total edge state as detail`() {
|
||
let now = Date(timeIntervalSince1970: 0)
|
||
let snap = KiloUsageSnapshot(
|
||
creditsUsed: 0,
|
||
creditsTotal: 0,
|
||
creditsRemaining: 0,
|
||
planName: "Kilo Pass Pro",
|
||
autoTopUpEnabled: true,
|
||
autoTopUpMethod: "visa",
|
||
updatedAt: now).toUsageSnapshot()
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .kilo,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Kilo (api)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Credits: 0% left"))
|
||
#expect(output.contains("0/0 credits"))
|
||
#expect(!output.contains("Resets 0/0 credits"))
|
||
}
|
||
|
||
@Test
|
||
func `renders kilo auto top up only as activity without plan`() {
|
||
let now = Date(timeIntervalSince1970: 0)
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .kilo,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: "Auto top-up: off")
|
||
let snap = UsageSnapshot(
|
||
primary: nil,
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now,
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .kilo,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Kilo (cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("Activity: Auto top-up: off"))
|
||
#expect(!output.contains("Plan: Auto top-up: off"))
|
||
}
|
||
|
||
@Test
|
||
func `renders pace line when weekly has reset`() {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: nil,
|
||
secondary: .init(
|
||
usedPercent: 50,
|
||
windowMinutes: 10080,
|
||
resetsAt: now.addingTimeInterval(3 * 24 * 60 * 60),
|
||
resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.0.0 (codex-cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown))
|
||
|
||
#expect(output.contains("Pace:"))
|
||
}
|
||
|
||
@Test
|
||
func `configured work days affect weekly text and JSON pace`() throws {
|
||
var calendar = Calendar.current
|
||
calendar.timeZone = .current
|
||
let resetsAt = try #require(calendar.date(from: DateComponents(
|
||
calendar: calendar,
|
||
timeZone: calendar.timeZone,
|
||
year: 2026,
|
||
month: 6,
|
||
day: 14)))
|
||
let now = resetsAt.addingTimeInterval(-72 * 60 * 60)
|
||
let snap = UsageSnapshot(
|
||
primary: nil,
|
||
secondary: .init(
|
||
usedPercent: 60,
|
||
windowMinutes: 10080,
|
||
resetsAt: resetsAt,
|
||
resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.0.0 (codex-cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown,
|
||
weeklyWorkDays: 5),
|
||
now: now)
|
||
#expect(output.contains("Pace: On pace | Expected 60% used | Lasts until reset"))
|
||
|
||
let pace = try #require(CLIRenderer.providerPacePayload(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
weeklyWorkDays: 5,
|
||
now: now)?.secondary)
|
||
#expect(pace.expectedUsedPercent == 60)
|
||
#expect(pace.summary == "On pace | Expected 60% used | Lasts until reset")
|
||
}
|
||
|
||
@Test
|
||
func `renders Ollama weekly pace line when weekly window has reset`() {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 0,
|
||
windowMinutes: nil,
|
||
resetsAt: now.addingTimeInterval(4 * 3600),
|
||
resetDescription: nil),
|
||
secondary: .init(
|
||
usedPercent: 23,
|
||
windowMinutes: 10080,
|
||
resetsAt: now.addingTimeInterval(5 * 24 * 3600),
|
||
resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .ollama,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Ollama (web)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown))
|
||
|
||
#expect(output.contains("Weekly: 77% left"))
|
||
#expect(output.contains("Pace: 6% in reserve | Expected 29% used | Lasts until reset"))
|
||
#expect(!output.contains("1.5× headroom"))
|
||
}
|
||
|
||
@Test
|
||
func `hides Ollama weekly pace when weekly duration is missing`() {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: nil,
|
||
secondary: .init(
|
||
usedPercent: 23,
|
||
windowMinutes: nil,
|
||
resetsAt: now.addingTimeInterval(5 * 24 * 3600),
|
||
resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .ollama,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Ollama (web)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown))
|
||
|
||
#expect(output.contains("Weekly: 77% left"))
|
||
#expect(!output.contains("Pace:"))
|
||
}
|
||
|
||
@Test
|
||
func `renders session pace line when session window has reset`() {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 20,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(2 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.0.0 (codex-cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown),
|
||
now: now)
|
||
|
||
#expect(output.contains("Session: 80% left"))
|
||
// 2h remaining of a 5h window => 3h elapsed => 60% expected; even rate easily lasts to reset.
|
||
#expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset | 1.5× headroom"))
|
||
}
|
||
|
||
@Test
|
||
func `renders Claude session pace using five hour default window`() {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 20,
|
||
windowMinutes: nil,
|
||
resetsAt: now.addingTimeInterval(2 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .claude,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Claude Code 2.0.69 (claude)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown),
|
||
now: now)
|
||
|
||
// windowMinutes is nil, so the 5-hour (300 minute) session default must drive the pace.
|
||
#expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset"))
|
||
#expect(!output.contains("1.5× headroom"))
|
||
}
|
||
|
||
@Test
|
||
func `renders session pace deficit with run out estimate`() {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 50,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(4 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.0.0 (codex-cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown),
|
||
now: now)
|
||
|
||
// 1h elapsed of a 5h window => 20% expected vs 50% used => burning ahead of pace.
|
||
// Session mirrors the GUI's "Projected empty" wording (weekly uses "Runs out").
|
||
#expect(output.contains("Pace: 30% in deficit | Expected 20% used | Projected empty in"))
|
||
#expect(!output.contains("Runs out"))
|
||
}
|
||
|
||
@Test
|
||
func `renders session pace on track and lasts until reset`() {
|
||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||
// Exactly halfway through a 5h window with 50% used => On pace (delta 0); the even rate
|
||
// means the quota lasts precisely to the reset.
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 50,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(2.5 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.0.0 (codex-cli)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown),
|
||
now: now)
|
||
|
||
#expect(output.contains("Pace: On pace | Expected 50% used | Lasts until reset"))
|
||
}
|
||
|
||
@Test
|
||
func `hides session pace for unsupported provider`() {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 20,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(2 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .zai,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "z.ai 0.0.0 (zai)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown))
|
||
|
||
#expect(!output.contains("Pace:"))
|
||
}
|
||
|
||
@Test
|
||
func `hides session pace for non-session primary window`() {
|
||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||
// Claude with no 5-hour data falls a 7-day window back into `primary`; it must not be
|
||
// paced as a "Session" (that would print "Projected empty …" over a weekly window).
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 50,
|
||
windowMinutes: 10080,
|
||
resetsAt: now.addingTimeInterval(3 * 24 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .claude,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Claude Code 2.0.69 (claude)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .countdown),
|
||
now: now)
|
||
|
||
#expect(!output.contains("Pace:"))
|
||
#expect(CLIRenderer.providerPacePayload(provider: .claude, snapshot: snap, now: now) == nil)
|
||
}
|
||
|
||
@Test
|
||
func `renders JSON payload`() throws {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 50, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: .init(usedPercent: 10, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
|
||
|
||
let payload = ProviderPayload(
|
||
provider: .codex,
|
||
account: nil,
|
||
version: "1.2.3",
|
||
source: "codex-cli",
|
||
status: ProviderStatusPayload(
|
||
indicator: .none,
|
||
description: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 1_700_000_010),
|
||
url: "https://status.example.com"),
|
||
usage: snap,
|
||
credits: nil,
|
||
antigravityPlanInfo: nil,
|
||
openaiDashboard: nil,
|
||
error: nil)
|
||
let encoder = JSONEncoder()
|
||
encoder.dateEncodingStrategy = .secondsSince1970
|
||
let data = try encoder.encode(payload)
|
||
guard let json = String(data: data, encoding: .utf8) else {
|
||
Issue.record("Failed to decode JSON payload")
|
||
return
|
||
}
|
||
|
||
#expect(json.contains("\"provider\":\"codex\""))
|
||
#expect(json.contains("\"version\":\"1.2.3\""))
|
||
#expect(json.contains("\"status\""))
|
||
#expect(json.contains("status.example.com"))
|
||
#expect(json.contains("\"primary\""))
|
||
#expect(json.contains("\"windowMinutes\":300"))
|
||
#expect(json.contains("1700000000"))
|
||
}
|
||
|
||
@Test
|
||
func `json pace rounds derived numbers to match usage precision`() throws {
|
||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||
// 13000s elapsed of an 18000s (300m) window => 72.22% expected; used 79 => +6.78 deficit;
|
||
// projected empty in ~3455.7s. Derived fields must be emitted as whole numbers (no float noise).
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 79,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(5000),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let payload = ProviderPayload(
|
||
provider: .codex,
|
||
account: nil,
|
||
version: nil,
|
||
source: "codex-cli",
|
||
status: nil,
|
||
usage: snap,
|
||
credits: nil,
|
||
antigravityPlanInfo: nil,
|
||
openaiDashboard: nil,
|
||
error: nil,
|
||
pace: CLIRenderer.providerPacePayload(provider: .codex, snapshot: snap, now: now))
|
||
|
||
let data = try JSONEncoder().encode(payload)
|
||
let root = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||
let primary = try #require((root["pace"] as? [String: Any])?["primary"] as? [String: Any])
|
||
|
||
#expect(primary["expectedUsedPercent"] as? Double == 72)
|
||
#expect(primary["deltaPercent"] as? Double == 7)
|
||
#expect(primary["etaSeconds"] as? Double == 3456)
|
||
// actualUsedPercent is not emitted; consumers read usage.primary.usedPercent.
|
||
#expect(primary["actualUsedPercent"] == nil)
|
||
}
|
||
|
||
@Test
|
||
func `json payload includes session and weekly pace with distinct wording`() throws {
|
||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||
let snap = UsageSnapshot(
|
||
// 1h elapsed of a 5h window => 20% expected vs 50% used => deficit, runs out in 1h.
|
||
primary: .init(
|
||
usedPercent: 50,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(4 * 60 * 60),
|
||
resetDescription: nil),
|
||
// 5d elapsed of a 7d window => ~71% expected vs 90% used => deficit, runs out before reset.
|
||
secondary: .init(
|
||
usedPercent: 90,
|
||
windowMinutes: 10080,
|
||
resetsAt: now.addingTimeInterval(2 * 24 * 60 * 60),
|
||
resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let payload = ProviderPayload(
|
||
provider: .codex,
|
||
account: nil,
|
||
version: "1.2.3",
|
||
source: "codex-cli",
|
||
status: nil,
|
||
usage: snap,
|
||
credits: nil,
|
||
antigravityPlanInfo: nil,
|
||
openaiDashboard: nil,
|
||
error: nil,
|
||
pace: CLIRenderer.providerPacePayload(provider: .codex, snapshot: snap, now: now))
|
||
|
||
let data = try JSONEncoder().encode(payload)
|
||
let root = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||
let pace = try #require(root["pace"] as? [String: Any])
|
||
|
||
let primary = try #require(pace["primary"] as? [String: Any])
|
||
#expect(primary["stage"] as? String == "farAhead")
|
||
#expect(primary["expectedUsedPercent"] as? Double == 20)
|
||
#expect(primary["deltaPercent"] as? Double == 30)
|
||
#expect(primary["willLastToReset"] as? Bool == false)
|
||
#expect(primary["etaSeconds"] as? Double == 3600)
|
||
#expect((primary["summary"] as? String)?
|
||
.contains("30% in deficit | Expected 20% used | Projected empty in") == true)
|
||
// actualUsedPercent is redundant with usage.usedPercent and is not emitted;
|
||
// runOutProbability is never set by the CLI, so both keys are omitted.
|
||
#expect(primary["actualUsedPercent"] == nil)
|
||
#expect(primary["runOutProbability"] == nil)
|
||
|
||
let secondary = try #require(pace["secondary"] as? [String: Any])
|
||
#expect(secondary["stage"] as? String == "farAhead")
|
||
#expect((secondary["summary"] as? String)?.contains("Runs out in") == true)
|
||
#expect((secondary["summary"] as? String)?.contains("Projected empty") == false)
|
||
}
|
||
|
||
@Test
|
||
func `json omits pace when not applicable`() throws {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 20,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(2 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
// z.ai is not a session/weekly pace provider, so no pace should be emitted.
|
||
let payload = ProviderPayload(
|
||
provider: .zai,
|
||
account: nil,
|
||
version: nil,
|
||
source: "zai",
|
||
status: nil,
|
||
usage: snap,
|
||
credits: nil,
|
||
antigravityPlanInfo: nil,
|
||
openaiDashboard: nil,
|
||
error: nil,
|
||
pace: CLIRenderer.providerPacePayload(provider: .zai, snapshot: snap, now: now))
|
||
|
||
let data = try JSONEncoder().encode(payload)
|
||
let json = try #require(String(data: data, encoding: .utf8))
|
||
#expect(!json.contains("\"pace\""))
|
||
}
|
||
|
||
@Test
|
||
func `json includes only session pace when weekly window missing`() throws {
|
||
let now = Date()
|
||
let snap = UsageSnapshot(
|
||
primary: .init(
|
||
usedPercent: 50,
|
||
windowMinutes: 300,
|
||
resetsAt: now.addingTimeInterval(4 * 60 * 60),
|
||
resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: now)
|
||
|
||
let payload = ProviderPayload(
|
||
provider: .codex,
|
||
account: nil,
|
||
version: nil,
|
||
source: "codex-cli",
|
||
status: nil,
|
||
usage: snap,
|
||
credits: nil,
|
||
antigravityPlanInfo: nil,
|
||
openaiDashboard: nil,
|
||
error: nil,
|
||
pace: CLIRenderer.providerPacePayload(provider: .codex, snapshot: snap, now: now))
|
||
|
||
let data = try JSONEncoder().encode(payload)
|
||
let root = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||
let pace = try #require(root["pace"] as? [String: Any])
|
||
#expect(pace["primary"] is [String: Any])
|
||
#expect(pace["secondary"] == nil)
|
||
}
|
||
|
||
@Test
|
||
func `encodes JSON with secondary null when missing`() throws {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
|
||
|
||
let encoder = JSONEncoder()
|
||
encoder.dateEncodingStrategy = .secondsSince1970
|
||
let data = try encoder.encode(snap)
|
||
guard let json = String(data: data, encoding: .utf8) else {
|
||
Issue.record("Failed to decode JSON payload")
|
||
return
|
||
}
|
||
|
||
#expect(json.contains("\"secondary\":null"))
|
||
}
|
||
|
||
@Test
|
||
func `parses output format`() {
|
||
#expect(OutputFormat(argument: "json") == .json)
|
||
#expect(OutputFormat(argument: "TEXT") == .text)
|
||
#expect(OutputFormat(argument: "invalid") == nil)
|
||
}
|
||
|
||
@Test
|
||
func `defaults to usage when no command provided`() {
|
||
#expect(CodexBarCLI.effectiveArgv([]) == ["usage"])
|
||
#expect(CodexBarCLI.effectiveArgv(["--format", "json"]).first == "usage")
|
||
#expect(CodexBarCLI.effectiveArgv(["usage", "--format", "json"]).first == "usage")
|
||
}
|
||
|
||
@Test
|
||
func `status line is last and colored when TTY`() {
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .claude,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: "pro")
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: .init(usedPercent: 0, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: Date(),
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .claude,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Claude Code 2.0.58 (claude)",
|
||
status: ProviderStatusPayload(
|
||
indicator: .critical,
|
||
description: "Major outage",
|
||
updatedAt: nil,
|
||
url: "https://status.claude.com"),
|
||
useColor: true,
|
||
resetStyle: .absolute))
|
||
|
||
let lines = output.split(separator: "\n")
|
||
#expect(lines.last?.contains("Status: Critical issue – Major outage") == true)
|
||
#expect(output.contains("\u{001B}[31mStatus")) // red for critical
|
||
}
|
||
|
||
@Test
|
||
func `output has ansi when TTY even without status`() {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 1, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: nil,
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.0.0 (codex-cli)",
|
||
status: nil,
|
||
useColor: true,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("\u{001B}["))
|
||
}
|
||
|
||
@Test
|
||
func `tty output colors header and usage`() {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 95, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: .init(usedPercent: 80, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.0.0 (codex-cli)",
|
||
status: nil,
|
||
useColor: true,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("\u{001B}[1;95m== Codex 0.0.0 (codex-cli) ==\u{001B}[0m"))
|
||
#expect(output.contains("Session: \u{001B}[31m5% left\u{001B}[0m")) // red <10% left
|
||
#expect(output.contains("Weekly: \u{001B}[33m20% left\u{001B}[0m")) // yellow <25% left
|
||
}
|
||
|
||
@Test
|
||
func `status line is plain when no TTY`() {
|
||
let identity = ProviderIdentitySnapshot(
|
||
providerID: .codex,
|
||
accountEmail: nil,
|
||
accountOrganization: nil,
|
||
loginMethod: "pro")
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
secondary: .init(usedPercent: 0, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
||
tertiary: nil,
|
||
updatedAt: Date(),
|
||
identity: identity)
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .codex,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Codex 0.6.0 (codex-cli)",
|
||
status: ProviderStatusPayload(
|
||
indicator: .none,
|
||
description: "Operational",
|
||
updatedAt: nil,
|
||
url: "https://status.openai.com/"),
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(!output.contains("\u{001B}["))
|
||
#expect(output.contains("Status: Operational – Operational"))
|
||
}
|
||
|
||
@Test
|
||
func `renders 5-hour tertiary row for zai`() {
|
||
let snap = UsageSnapshot(
|
||
primary: .init(usedPercent: 9, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
||
secondary: .init(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
||
tertiary: .init(usedPercent: 25, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
|
||
let output = CLIRenderer.renderText(
|
||
provider: .zai,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "z.ai 0.0.0 (zai)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
|
||
#expect(output.contains("5-hour:"))
|
||
#expect(output.contains("Tokens:"))
|
||
#expect(output.contains("MCP:"))
|
||
}
|
||
|
||
@Test
|
||
func `devin overage balance without primary window omits generic cost line`() {
|
||
let snap = UsageSnapshot(
|
||
primary: nil,
|
||
secondary: .init(usedPercent: 42, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
||
tertiary: nil,
|
||
providerCost: ProviderCostSnapshot(
|
||
used: 48.0,
|
||
limit: 0,
|
||
currencyCode: "USD",
|
||
period: "Extra usage balance",
|
||
updatedAt: Date(timeIntervalSince1970: 0)),
|
||
updatedAt: Date(timeIntervalSince1970: 0))
|
||
let output = CLIRenderer.renderText(
|
||
provider: .devin,
|
||
snapshot: snap,
|
||
credits: nil,
|
||
context: RenderContext(
|
||
header: "Devin (devin)",
|
||
status: nil,
|
||
useColor: false,
|
||
resetStyle: .absolute))
|
||
#expect(output.contains("Extra usage: $48.00"))
|
||
#expect(!output.contains("Cost:"))
|
||
#expect(!output.contains(" / 0.0"))
|
||
}
|
||
}
|