@@ -16,7 +16,7 @@ const CREATE_NO_WINDOW: u32 = 0x08000000;
1616
1717const MODEL_FALLBACK_CHAIN : & [ & str ] = & [ "claude-3-haiku-20240307" , "claude-haiku-4-5-20251001" ] ;
1818
19- #[ derive( Debug ) ]
19+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
2020pub enum PollError {
2121 AuthRequired ,
2222 NoCredentials ,
@@ -73,22 +73,44 @@ struct CodexRateLimitWindow {
7373}
7474
7575pub fn poll ( show_claude_code : bool , show_codex : bool ) -> Result < AppUsageData , PollError > {
76+ poll_with ( show_claude_code, show_codex, poll_claude_code, poll_codex)
77+ }
78+
79+ fn poll_with (
80+ show_claude_code : bool ,
81+ show_codex : bool ,
82+ mut poll_claude_code : impl FnMut ( ) -> Result < UsageData , PollError > ,
83+ mut poll_codex : impl FnMut ( ) -> Result < UsageData , PollError > ,
84+ ) -> Result < AppUsageData , PollError > {
7685 let mut data = AppUsageData :: default ( ) ;
86+ let mut first_error = None ;
7787
7888 if show_claude_code {
79- data. claude_code = Some ( poll_claude_code ( ) ?) ;
89+ match poll_claude_code ( ) {
90+ Ok ( claude_code) => data. claude_code = Some ( claude_code) ,
91+ Err ( error) => {
92+ if show_codex {
93+ diagnose:: log ( format ! ( "Claude Code usage poll failed: {error:?}" ) ) ;
94+ }
95+ first_error. get_or_insert ( error) ;
96+ }
97+ }
8098 }
8199
82100 if show_codex {
83101 match poll_codex ( ) {
84102 Ok ( codex) => data. codex = Some ( codex) ,
85- Err ( error) if !show_claude_code => return Err ( error) ,
86- Err ( error) => diagnose:: log ( format ! ( "Codex usage poll failed: {error:?}" ) ) ,
103+ Err ( error) => {
104+ if show_claude_code {
105+ diagnose:: log ( format ! ( "Codex usage poll failed: {error:?}" ) ) ;
106+ }
107+ first_error. get_or_insert ( error) ;
108+ }
87109 }
88110 }
89111
90112 if data. claude_code . is_none ( ) && data. codex . is_none ( ) {
91- Err ( PollError :: RequestFailed )
113+ Err ( first_error . unwrap_or ( PollError :: RequestFailed ) )
92114 } else {
93115 Ok ( data)
94116 }
@@ -1097,3 +1119,59 @@ pub fn app_is_past_reset(data: &AppUsageData) -> bool {
10971119 data. claude_code . as_ref ( ) . is_some_and ( is_past_reset)
10981120 || data. codex . as_ref ( ) . is_some_and ( is_past_reset)
10991121}
1122+
1123+ #[ cfg( test) ]
1124+ mod tests {
1125+ use super :: * ;
1126+
1127+ fn usage_with_session_percent ( percentage : f64 ) -> UsageData {
1128+ UsageData {
1129+ session : UsageSection {
1130+ percentage,
1131+ resets_at : None ,
1132+ } ,
1133+ weekly : UsageSection :: default ( ) ,
1134+ }
1135+ }
1136+
1137+ #[ test]
1138+ fn claude_failure_does_not_block_codex_when_both_are_enabled ( ) {
1139+ let data = poll_with (
1140+ true ,
1141+ true ,
1142+ || Err ( PollError :: AuthRequired ) ,
1143+ || Ok ( usage_with_session_percent ( 42.0 ) ) ,
1144+ )
1145+ . expect ( "codex data should keep the poll successful" ) ;
1146+
1147+ assert ! ( data. claude_code. is_none( ) ) ;
1148+ assert_eq ! ( data. codex. unwrap( ) . session. percentage, 42.0 ) ;
1149+ }
1150+
1151+ #[ test]
1152+ fn codex_failure_does_not_block_claude_when_both_are_enabled ( ) {
1153+ let data = poll_with (
1154+ true ,
1155+ true ,
1156+ || Ok ( usage_with_session_percent ( 64.0 ) ) ,
1157+ || Err ( PollError :: RequestFailed ) ,
1158+ )
1159+ . expect ( "claude data should keep the poll successful" ) ;
1160+
1161+ assert_eq ! ( data. claude_code. unwrap( ) . session. percentage, 64.0 ) ;
1162+ assert ! ( data. codex. is_none( ) ) ;
1163+ }
1164+
1165+ #[ test]
1166+ fn returns_first_error_when_no_enabled_provider_succeeds ( ) {
1167+ let error = poll_with (
1168+ true ,
1169+ true ,
1170+ || Err ( PollError :: AuthRequired ) ,
1171+ || Err ( PollError :: RequestFailed ) ,
1172+ )
1173+ . expect_err ( "all-provider failure should return an error" ) ;
1174+
1175+ assert_eq ! ( error, PollError :: AuthRequired ) ;
1176+ }
1177+ }
0 commit comments