843 lines
33 KiB
Swift
843 lines
33 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
struct StatusProbeTests {
|
|
@Test
|
|
func `parse codex status`() throws {
|
|
let sample = """
|
|
Model: gpt
|
|
Credits: 980 credits
|
|
5h limit: [#####] 75% left
|
|
Weekly limit: [##] 25% left
|
|
"""
|
|
let snap = try CodexStatusProbe.parse(text: sample)
|
|
#expect(snap.credits == 980)
|
|
#expect(snap.fiveHourPercentLeft == 75)
|
|
#expect(snap.weeklyPercentLeft == 25)
|
|
}
|
|
|
|
@Test
|
|
func `parse codex status with ansi and resets`() throws {
|
|
let now = try #require(
|
|
Calendar(identifier: .gregorian).date(from: DateComponents(
|
|
timeZone: TimeZone.current,
|
|
year: 2026,
|
|
month: 11,
|
|
day: 26,
|
|
hour: 8,
|
|
minute: 0)))
|
|
let sample = """
|
|
\u{001B}[38;5;245mCredits:\u{001B}[0m 557 credits
|
|
5h limit: [█████ ] 50% left (resets 09:01)
|
|
Weekly limit: [███████ ] 85% left (resets 04:01 on 27 Nov)
|
|
"""
|
|
let snap = try CodexStatusProbe.parse(text: sample, now: now)
|
|
#expect(snap.credits == 557)
|
|
#expect(snap.fiveHourPercentLeft == 50)
|
|
#expect(snap.weeklyPercentLeft == 85)
|
|
#expect(snap.fiveHourResetsAt == Calendar(identifier: .gregorian).date(from: DateComponents(
|
|
timeZone: TimeZone.current,
|
|
year: 2026,
|
|
month: 11,
|
|
day: 26,
|
|
hour: 9,
|
|
minute: 1)))
|
|
#expect(snap.weeklyResetsAt == Calendar(identifier: .gregorian).date(from: DateComponents(
|
|
timeZone: TimeZone.current,
|
|
year: 2026,
|
|
month: 11,
|
|
day: 27,
|
|
hour: 4,
|
|
minute: 1)))
|
|
}
|
|
|
|
@Test
|
|
func `parse codex status with weekly only line`() throws {
|
|
let sample = """
|
|
Model: gpt
|
|
Credits: 980 credits
|
|
Weekly limit: [##] 25% left
|
|
"""
|
|
let snap = try CodexStatusProbe.parse(text: sample)
|
|
#expect(snap.credits == 980)
|
|
#expect(snap.fiveHourPercentLeft == nil)
|
|
#expect(snap.weeklyPercentLeft == 25)
|
|
}
|
|
|
|
@Test
|
|
func `parse codex monthly credit limit`() throws {
|
|
let now = try #require(
|
|
Calendar(identifier: .gregorian).date(from: DateComponents(
|
|
timeZone: TimeZone.current,
|
|
year: 2026,
|
|
month: 6,
|
|
day: 23,
|
|
hour: 12,
|
|
minute: 0)))
|
|
let sample = """
|
|
Model: codex-status-fixture
|
|
Monthly credit limit: [██████████████████░░] 92% left (resets 08:00 on 1 Jul)
|
|
7,761 of 100,000 credits used
|
|
"""
|
|
|
|
let snap = try CodexStatusProbe.parse(text: sample, now: now)
|
|
|
|
#expect(snap.codexCreditLimit?.limit == 100_000)
|
|
#expect(snap.codexCreditLimit?.used == 7761)
|
|
#expect(snap.codexCreditLimit?.remaining == 92239)
|
|
#expect(snap.codexCreditLimit?.remainingPercent == 92)
|
|
#expect(snap.codexCreditLimit?.resetsAt == Calendar(identifier: .gregorian).date(from: DateComponents(
|
|
timeZone: TimeZone.current,
|
|
year: 2026,
|
|
month: 7,
|
|
day: 1,
|
|
hour: 8,
|
|
minute: 0)))
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status`() throws {
|
|
let sample = """
|
|
Settings: Status Config Usage (tab to cycle)
|
|
|
|
Current session
|
|
1% used (Resets 5am (Europe/Vienna))
|
|
Current week (all models)
|
|
1% used (Resets Dec 2 at 12am (Europe/Vienna))
|
|
Current week (Sonnet only)
|
|
1% used (Resets Dec 2 at 12am (Europe/Vienna))
|
|
|
|
Nov 24, 2025 update:
|
|
We've increased your limits and removed the Opus cap,
|
|
so you can use Opus 4.5 up to your overall limit.
|
|
Sonnet now has its own limit—it's set to match your previous overall limit,
|
|
so you can use just as much as before.
|
|
Account: user@example.com
|
|
Org: Example Org
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 99)
|
|
#expect(snap.weeklyPercentLeft == 99)
|
|
#expect(snap.opusPercentLeft == 99)
|
|
#expect(snap.accountEmail == "user@example.com")
|
|
#expect(snap.accountOrganization == "Example Org")
|
|
#expect(snap.primaryResetDescription == "Resets 5am (Europe/Vienna)")
|
|
#expect(snap.secondaryResetDescription == "Resets Dec 2 at 12am (Europe/Vienna)")
|
|
#expect(snap.opusResetDescription == "Resets Dec 2 at 12am (Europe/Vienna)")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status with ANSI`() throws {
|
|
let sample = """
|
|
\u{001B}[35mCurrent session\u{001B}[0m
|
|
40% used (Resets 11am)
|
|
Current week (all models)
|
|
10% used (Resets Nov 27)
|
|
Current week (Sonnet only)
|
|
0% used (Resets Nov 27)
|
|
Account: user@example.com
|
|
Org: ACME
|
|
\u{001B}[0m
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 60)
|
|
#expect(snap.weeklyPercentLeft == 90)
|
|
#expect(snap.opusPercentLeft == 100)
|
|
#expect(snap.primaryResetDescription == "Resets 11am")
|
|
#expect(snap.secondaryResetDescription == "Resets Nov 27")
|
|
#expect(snap.opusResetDescription == "Resets Nov 27")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status legacy opus label`() throws {
|
|
let sample = """
|
|
Current session
|
|
12% used (Resets 11am)
|
|
Current week (all models)
|
|
55% used (Resets Nov 21)
|
|
Current week (Opus)
|
|
5% used (Resets Nov 21)
|
|
Account: user@example.com
|
|
Org: Example Org
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 88)
|
|
#expect(snap.weeklyPercentLeft == 45)
|
|
#expect(snap.opusPercentLeft == 95)
|
|
#expect(snap.primaryResetDescription == "Resets 11am")
|
|
#expect(snap.secondaryResetDescription == "Resets Nov 21")
|
|
#expect(snap.opusResetDescription == "Resets Nov 21")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status remaining keyword`() throws {
|
|
let sample = """
|
|
Current session
|
|
12% remaining (Resets 11am)
|
|
Current week (all models)
|
|
40% remaining (Resets Nov 21)
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 12)
|
|
#expect(snap.weeklyPercentLeft == 40)
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status enterprise session only`() throws {
|
|
let sample = """
|
|
Current session
|
|
█ 2% used
|
|
Resets 3pm (Europe/Vienna)
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 98)
|
|
#expect(snap.weeklyPercentLeft == nil)
|
|
#expect(snap.primaryResetDescription == "Resets 3pm (Europe/Vienna)")
|
|
#expect(snap.secondaryResetDescription == nil)
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status reset mappings with CR line endings`() throws {
|
|
let sample =
|
|
"Current session\r" +
|
|
"██████████████████████████████████████████████████ 17% used\r" +
|
|
"Resets 12:59pm (Europe/Paris)\r" +
|
|
"Current week (all models)\r" +
|
|
"██████████████████████████████████████████████████ 4% used\r" +
|
|
"Resets Dec 24 at 3:59pm (Europe/Paris)\r" +
|
|
"Current week (Sonnet only)\r" +
|
|
"██████████████████████████████████████████████████ 3% used\r" +
|
|
"Resets Dec 23 at 3:59am (Europe/Paris)\r"
|
|
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 83)
|
|
#expect(snap.weeklyPercentLeft == 96)
|
|
#expect(snap.opusPercentLeft == 97)
|
|
#expect(snap.primaryResetDescription == "Resets 12:59pm (Europe/Paris)")
|
|
#expect(snap.secondaryResetDescription == "Resets Dec 24 at 3:59pm (Europe/Paris)")
|
|
#expect(snap.opusResetDescription == "Resets Dec 23 at 3:59am (Europe/Paris)")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status reset mappings does not promote weekly reset to session`() throws {
|
|
let sample = """
|
|
Current session
|
|
██████████████████████████████████████████████████ 17% used
|
|
Current week (all models)
|
|
██████████████████████████████████████████████████ 4% used
|
|
Resets Dec 24 at 3:59pm (Europe/Paris)
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 83)
|
|
#expect(snap.weeklyPercentLeft == 96)
|
|
#expect(snap.primaryResetDescription == nil)
|
|
#expect(snap.secondaryResetDescription == "Resets Dec 24 at 3:59pm (Europe/Paris)")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status with plan and ansi noise`() throws {
|
|
let sample = """
|
|
Settings: Status Config Usage
|
|
|
|
Login method: \u{001B}[22mClaude Max Account\u{001B}[0m
|
|
Account: user@example.com
|
|
Org: ACME
|
|
"""
|
|
// Only care about login/identity; include minimal usage lines to satisfy parser.
|
|
let text = """
|
|
Current session
|
|
10% used
|
|
Current week (all models)
|
|
20% used
|
|
Current week (Opus)
|
|
30% used
|
|
\(sample)
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: text)
|
|
#expect(snap.loginMethod == "Max")
|
|
#expect(snap.accountEmail == "user@example.com")
|
|
#expect(snap.accountOrganization == "ACME")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status with extra usage section`() throws {
|
|
let sample = """
|
|
Settings: Status Config Usage (tab to cycle)
|
|
|
|
Current session
|
|
▌ 1% used
|
|
Resets 3:59pm (Europe/Helsinki)
|
|
|
|
Current week (all models)
|
|
▌ 1% used
|
|
Resets Jan 2, 2026, 10:59pm (Europe/Helsinki)
|
|
|
|
Current week (Sonnet only)
|
|
0% used
|
|
|
|
Extra usage
|
|
Extra usage not enabled • /extra-usage to enable
|
|
"""
|
|
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 99)
|
|
#expect(snap.weeklyPercentLeft == 99)
|
|
#expect(snap.opusPercentLeft == 100)
|
|
#expect(snap.primaryResetDescription == "Resets 3:59pm (Europe/Helsinki)")
|
|
#expect(snap.secondaryResetDescription == "Resets Jan 2, 2026, 10:59pm (Europe/Helsinki)")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status ignores status bar context percent`() throws {
|
|
let sample = """
|
|
Claude Code v2.1.29
|
|
22:47 | | Opus 4.5 | default | ░░░░░░░░░░ 0% ◯ /ide for Visual Studio Code
|
|
|
|
Settings: Status Config Usage (tab to cycle)
|
|
Loading usage data…
|
|
Esc to cancel
|
|
|
|
Curretsession
|
|
███████▌15%used
|
|
Resets 11:30pm (Asia/Calcutta)
|
|
|
|
Current week (all models)
|
|
█▌ 3% used
|
|
Resets Feb 12 at 1:30pm (Asia/Calcutta)
|
|
|
|
Current week (Sonnet only)
|
|
▌ 1% used
|
|
Resets Feb 12 at 1:30pm (Asia/Calcutta)
|
|
"""
|
|
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 85)
|
|
#expect(snap.weeklyPercentLeft == 97)
|
|
#expect(snap.opusPercentLeft == 99)
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status loading panel surfaces loading stall`() {
|
|
let sample = """
|
|
Claude Code v2.1.29
|
|
22:47 | | Opus 4.5 | default | ░░░░░░░░░░ 0% ◯ /ide for Visual Studio Code
|
|
|
|
Settings: Status Config Usage (tab to cycle)
|
|
Loading usage data…
|
|
Esc to cancel
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail while /usage is still loading")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
#expect(message.lowercased().contains("loading"))
|
|
return
|
|
} catch ClaudeStatusProbeError.timedOut {
|
|
return
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `parse claude retained usage panel classifies latest loading panel`() {
|
|
let sample = """
|
|
Settings: Status Config Usage (tab to cycle)
|
|
Current session
|
|
███████▌15%used
|
|
Resets 11:30pm (Asia/Calcutta)
|
|
|
|
Current week (all models)
|
|
█▌ 3% used
|
|
Resets Feb 12 at 1:30pm (Asia/Calcutta)
|
|
|
|
Settings: Status Config Usage (tab to cycle)
|
|
Loading usage data…
|
|
Esc to cancel
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail while the latest /usage panel is still loading")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
#expect(message.lowercased().contains("loading"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status status only output does not fallback to zero`() {
|
|
let sample = """
|
|
Claude Code v2.1.32
|
|
01:07 | | Opus 4.6 | default | ░░░░░░░░░░ 0% left
|
|
Status: Partially Degraded Service
|
|
/status
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail when /usage windows are missing")
|
|
} catch ClaudeStatusProbeError.parseFailed {
|
|
return
|
|
} catch ClaudeStatusProbeError.timedOut {
|
|
return
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status placeholder usage window does not use status bar percent`() {
|
|
let sample = """
|
|
Claude Code v2.1.32
|
|
01:07 | | Opus 4.6 | default | ░░░░░░░░░░ 0% left
|
|
Settings: Status Config Usage
|
|
Current session
|
|
Current week (all models)
|
|
Current week (Sonnet only)
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail when only status-bar percentages are present")
|
|
} catch ClaudeStatusProbeError.parseFailed {
|
|
return
|
|
} catch ClaudeStatusProbeError.timedOut {
|
|
return
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status compact markers still parse`() throws {
|
|
let sample = """
|
|
Settings:StatusConfigUsage(←/→ortabtocycle)
|
|
Loadingusagedata…
|
|
Curretsession
|
|
███6%used
|
|
Resets4:29am(Asia/Calcutta)
|
|
Currentweek(allmodels)
|
|
██4%used
|
|
ResetsFeb12at1:29pm(Asia/Calcutta)
|
|
Currentweek(Sonnetonly)
|
|
▌1%used
|
|
ResetsFeb12at1:29pm(Asia/Calcutta)
|
|
"""
|
|
|
|
let snap = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(snap.sessionPercentLeft == 94)
|
|
#expect(snap.weeklyPercentLeft == 96)
|
|
#expect(snap.opusPercentLeft == 99)
|
|
#expect(snap.secondaryResetDescription == "ResetsFeb12at1:29pm(Asia/Calcutta)")
|
|
#expect(snap.opusResetDescription == "ResetsFeb12at1:29pm(Asia/Calcutta)")
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status with bracket plan noise no esc`() throws {
|
|
let sample = """
|
|
Login method: [22m Claude Max Account
|
|
Account: user@example.com
|
|
"""
|
|
let text = """
|
|
Current session
|
|
10% used
|
|
Current week (all models)
|
|
20% used
|
|
Current week (Opus)
|
|
30% used
|
|
\(sample)
|
|
"""
|
|
let snap = try ClaudeStatusProbe.parse(text: text)
|
|
#expect(snap.loginMethod == "Max")
|
|
}
|
|
|
|
@Test
|
|
func `surfaces claude token expired`() {
|
|
let sample = """
|
|
Settings: Status Config Usage
|
|
|
|
Error: Failed to load usage data: {"type":"error","error":{"type":"authentication_error",
|
|
"message":"OAuth token has expired. Please obtain a new token or refresh your existing token.",
|
|
"details":{"error_visibility":"user_facing","error_code":"token_expired"}},\
|
|
"request_id":"req_123"}
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail for auth error")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
let lower = message.lowercased()
|
|
#expect(lower.contains("token"))
|
|
#expect(lower.contains("login"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `surfaces claude rate limited compact usage error`() {
|
|
let sample = """
|
|
Settings:StatusConfigUsage(←/→ortabtocycle)
|
|
Error:Failedtoloadusagedata:{"error":{"message":"Ratelimited.Pleasetryagainlater.","type":"rate_limit_error"}}
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail for rate limiting")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
let lower = message.lowercased()
|
|
#expect(lower.contains("rate"))
|
|
#expect(lower.contains("limit"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `surfaces claude folder trust prompt`() {
|
|
let sample = """
|
|
Do you trust the files in this folder?
|
|
|
|
/Users/example/project
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail for folder trust prompt")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
#expect(message.lowercased().contains("trust"))
|
|
#expect(message.contains("/Users/example/project"))
|
|
#expect(message.contains("cd \"/Users/example/project\" && claude"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `surfaces claude folder trust prompt with CRLF and spaces`() {
|
|
let sample = "Do you trust the files in this folder?\r\n\r\n/Users/example/My Project\r\n"
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail for folder trust prompt")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
#expect(message.contains("/Users/example/My Project"))
|
|
#expect(message.contains("cd \"/Users/example/My Project\" && claude"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `surfaces claude folder trust prompt without folder path`() {
|
|
let sample = """
|
|
Do you trust the files in this folder?
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail for folder trust prompt")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
let lower = message.lowercased()
|
|
#expect(lower.contains("trust"))
|
|
#expect(lower.contains("auto-accept"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `surfaces claude subscription notice without quota data`() {
|
|
let sample = """
|
|
You are currently using your subscription to power your Claude Code usage
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: sample)
|
|
#expect(Bool(false), "Parsing should fail for subscription notice without quota data")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
let lower = message.lowercased()
|
|
#expect(lower.contains("subscription"))
|
|
#expect(!lower.contains("still loading"))
|
|
#expect(ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(message))
|
|
|
|
let errorDescription = ClaudeStatusProbeError.parseFailed(message).localizedDescription
|
|
#expect(UsageLimitsAvailability.resolve(
|
|
provider: .claude,
|
|
snapshot: nil,
|
|
lastErrorDescription: errorDescription) == .unavailable)
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status subscription notice is distinct from loading stall`() {
|
|
let subscriptionOnly = "You are currently using your subscription to power your Claude Code usage"
|
|
let loadingOnly = """
|
|
Settings: Status Config Usage (tab to cycle)
|
|
Loading usage data…
|
|
Esc to cancel
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: subscriptionOnly)
|
|
#expect(Bool(false), "Subscription notice should fail parsing")
|
|
} catch let ClaudeStatusProbeError.parseFailed(subMessage) {
|
|
#expect(!subMessage.lowercased().contains("still loading"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error for subscription: \(error)")
|
|
}
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: loadingOnly)
|
|
#expect(Bool(false), "Loading panel should fail parsing")
|
|
} catch let ClaudeStatusProbeError.parseFailed(loadMessage) {
|
|
#expect(loadMessage.lowercased().contains("loading"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error for loading: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `parse claude status mixed loading and subscription notice surfaces subscription error`() {
|
|
// PTY capture containing both an intermediate "Loading usage data…" panel and the final
|
|
// Claude CLI 2.1.148 subscription notice. The subscription error must be surfaced, not
|
|
// the still-loading stall, so the UI shows the precise subscription message.
|
|
let mixedCapture = """
|
|
Settings: Status Config Usage (tab to cycle)
|
|
Loading usage data…
|
|
Esc to cancel
|
|
|
|
You are currently using your subscription to power your Claude Code usage
|
|
"""
|
|
|
|
do {
|
|
_ = try ClaudeStatusProbe.parse(text: mixedCapture)
|
|
#expect(Bool(false), "Parsing should fail for mixed loading+subscription capture")
|
|
} catch let ClaudeStatusProbeError.parseFailed(message) {
|
|
let lower = message.lowercased()
|
|
#expect(lower.contains("subscription"))
|
|
#expect(!lower.contains("still loading"))
|
|
} catch {
|
|
#expect(Bool(false), "Unexpected error for mixed capture: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `uses the five hour window to resolve stale claude reset times`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
let cases: [(now: DateComponents, text: String, expected: DateComponents)] = [
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 15),
|
|
"Resets 3pm (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 15)),
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 15, minute: 5),
|
|
"Resets 3pm (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 15, minute: 0)),
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 20),
|
|
"Resets 3pm (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 15)),
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 23, minute: 59),
|
|
"Resets 12:01am (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 10, hour: 0, minute: 1)),
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 10, hour: 0, minute: 1),
|
|
"Resets 11:59pm (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 23, minute: 59)),
|
|
]
|
|
|
|
for item in cases {
|
|
let now = try #require(calendar.date(from: item.now))
|
|
let parsed = ClaudeStatusProbe.parseResetDate(
|
|
from: item.text,
|
|
now: now,
|
|
expectedWindow: 5 * 60 * 60)
|
|
#expect(parsed == calendar.date(from: item.expected), "Failed session-window resolution: \(item.text)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `parses claude reset date and time`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_733_690_000)
|
|
let parsed = ClaudeStatusProbe.parseResetDate(from: "Resets Dec 9, 8:59am (Europe/Helsinki)", now: now)
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "Europe/Helsinki"))
|
|
let expected = calendar.date(from: DateComponents(
|
|
year: calendar.component(.year, from: now),
|
|
month: 12,
|
|
day: 9,
|
|
hour: 8,
|
|
minute: 59,
|
|
second: 0))
|
|
#expect(parsed == expected)
|
|
}
|
|
|
|
@Test
|
|
func `uses the weekly window to resolve stale claude reset dates`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
let cases: [(now: DateComponents, text: String, expected: DateComponents)] = [
|
|
(
|
|
DateComponents(year: 2026, month: 12, day: 31, hour: 23),
|
|
"Resets Jan 2, 3:15am (UTC)",
|
|
DateComponents(year: 2027, month: 1, day: 2, hour: 3, minute: 15)),
|
|
(
|
|
DateComponents(year: 2026, month: 12, day: 31, hour: 23),
|
|
"Resets Jan 2, 3am (UTC)",
|
|
DateComponents(year: 2027, month: 1, day: 2, hour: 3, minute: 0)),
|
|
(
|
|
DateComponents(year: 2027, month: 1, day: 1, hour: 0, minute: 5),
|
|
"Resets Dec 31, 11:59pm (UTC)",
|
|
DateComponents(year: 2026, month: 12, day: 31, hour: 23, minute: 59)),
|
|
(
|
|
DateComponents(year: 2027, month: 1, day: 1, hour: 0, minute: 5),
|
|
"Resets Dec 31, 11pm (UTC)",
|
|
DateComponents(year: 2026, month: 12, day: 31, hour: 23, minute: 0)),
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 15, minute: 5),
|
|
"Resets Jul 9, 3:00pm (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 15, minute: 0)),
|
|
]
|
|
|
|
for item in cases {
|
|
let now = try #require(calendar.date(from: item.now))
|
|
let parsed = ClaudeStatusProbe.parseResetDate(
|
|
from: item.text,
|
|
now: now,
|
|
expectedWindow: 7 * 24 * 60 * 60)
|
|
#expect(parsed == calendar.date(from: item.expected), "Failed weekly-window resolution: \(item.text)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `public claude reset parser remains forward looking`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
let cases: [(now: DateComponents, text: String, expected: DateComponents)] = [
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 8),
|
|
"Resets 9pm (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 21)),
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 20),
|
|
"Resets 1pm (UTC)",
|
|
DateComponents(year: 2026, month: 7, day: 10, hour: 13)),
|
|
(
|
|
DateComponents(year: 2026, month: 7, day: 9, hour: 12),
|
|
"Resets Jul 1, 9am (UTC)",
|
|
DateComponents(year: 2027, month: 7, day: 1, hour: 9)),
|
|
]
|
|
|
|
for item in cases {
|
|
let now = try #require(calendar.date(from: item.now))
|
|
let parsed = ClaudeStatusProbe.parseResetDate(from: item.text, now: now)
|
|
#expect(parsed == calendar.date(from: item.expected), "Failed future resolution: \(item.text)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `stale same day claude reset renders resets now`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
let now = try #require(calendar.date(from: DateComponents(
|
|
year: 2026, month: 7, day: 9, hour: 15, minute: 5, second: 0)))
|
|
let resetText = "Resets Jul 9, 3:00pm (UTC)"
|
|
let resetDate = try #require(ClaudeStatusProbe.parseResetDate(
|
|
from: resetText,
|
|
now: now,
|
|
expectedWindow: 5 * 60 * 60))
|
|
let window = RateWindow(
|
|
usedPercent: 73,
|
|
windowMinutes: 5 * 60,
|
|
resetsAt: resetDate,
|
|
resetDescription: resetText)
|
|
|
|
#expect(UsageFormatter.resetLine(for: window, style: .countdown, now: now) == "Resets now")
|
|
}
|
|
|
|
@Test
|
|
func `parses claude reset with dot separated time`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_733_690_000)
|
|
let parsed = ClaudeStatusProbe.parseResetDate(from: "Resets Dec 9 at 5.27am (UTC)", now: now)
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
let expected = calendar.date(from: DateComponents(year: 2024, month: 12, day: 9, hour: 5, minute: 27))
|
|
#expect(parsed == expected)
|
|
}
|
|
|
|
@Test
|
|
func `parses claude reset with compact times`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_733_690_000)
|
|
let parsedTimeOnly = ClaudeStatusProbe.parseResetDate(from: "Resets 1pm (UTC)", now: now)
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "UTC"))
|
|
let sameDay = try #require(calendar.date(bySettingHour: 13, minute: 0, second: 0, of: now))
|
|
let expected = try #require(calendar.date(byAdding: .day, value: 1, to: sameDay))
|
|
#expect(parsedTimeOnly == expected)
|
|
|
|
let parsedDateTime = ClaudeStatusProbe.parseResetDate(from: "Resets Dec 9, 9am", now: now)
|
|
calendar.timeZone = TimeZone.current
|
|
let dateExpected = calendar.date(from: DateComponents(
|
|
year: calendar.component(.year, from: now),
|
|
month: 12,
|
|
day: 9,
|
|
hour: 9,
|
|
minute: 0,
|
|
second: 0))
|
|
#expect(parsedDateTime == dateExpected)
|
|
}
|
|
|
|
@Test
|
|
func `parses claude reset with compact date and time no spaces`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_773_097_200) // Mar 10, 2026 12:00:00 UTC
|
|
let parsed = ClaudeStatusProbe.parseResetDate(from: "ResetsMar13at12:30pm(Asia/Calcutta)", now: now)
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(identifier: "Asia/Calcutta"))
|
|
let expected = calendar.date(from: DateComponents(year: 2026, month: 3, day: 13, hour: 12, minute: 30))
|
|
#expect(parsed == expected)
|
|
}
|
|
|
|
@Test
|
|
func `live codex status`() async throws {
|
|
guard ProcessInfo.processInfo.environment["LIVE_CODEX_STATUS"] == "1" else { return }
|
|
|
|
let probe = CodexStatusProbe()
|
|
do {
|
|
let snap = try await probe.fetch()
|
|
let summary = """
|
|
Live Codex status:
|
|
\(snap.rawText)
|
|
values: 5h \(snap.fiveHourPercentLeft ?? -1)% left,
|
|
weekly \(snap.weeklyPercentLeft ?? -1)% left,
|
|
credits \(snap.credits ?? -1)
|
|
"""
|
|
print(summary)
|
|
} catch {
|
|
// Dump raw PTY text to help debug.
|
|
let runner = TTYCommandRunner()
|
|
let res = try runner.run(
|
|
binary: "codex",
|
|
send: "/status\n",
|
|
options: .init(rows: 60, cols: 200, timeout: 12))
|
|
print("RAW CODEX PTY OUTPUT BEGIN\n\(res.text)\nRAW CODEX PTY OUTPUT END")
|
|
let clean = TextParsing.stripANSICodes(res.text)
|
|
print("CLEAN CODEX OUTPUT BEGIN\n\(clean)\nCLEAN CODEX OUTPUT END")
|
|
let five = TextParsing.firstInt(pattern: #"5h limit[^\n]*?([0-9]{1,3})%\s+left"#, text: clean) ?? -1
|
|
let week = TextParsing.firstInt(pattern: #"Weekly limit[^\n]*?([0-9]{1,3})%\s+left"#, text: clean) ?? -1
|
|
let credits = TextParsing.firstNumber(pattern: #"Credits:\s*([0-9][0-9.,]*)"#, text: clean) ?? -1
|
|
print("Parsed probes => 5h \(five)% weekly \(week)% credits \(credits)")
|
|
throw error
|
|
}
|
|
}
|
|
}
|