@@ -641,3 +641,66 @@ fn test_account_usage_probe_detects_all_accounts_exhausted() {
641641 assert ! ( probe. best_available_alternative( ) . is_none( ) ) ;
642642 assert ! ( probe. switch_guidance( ) . is_none( ) ) ;
643643}
644+
645+ #[ test]
646+ fn test_normalize_ratio_value_treats_low_integer_values_as_percent ( ) {
647+ // Direct unit test for the normalization helper to lock in the contract:
648+ // `wham/usage` and OpenAI account-usage endpoints report `used_percent`
649+ // in `[0, 100]`. Inputs must always be divided by 100, never treated as
650+ // an already-normalized ratio.
651+ assert ! ( ( openai_helpers:: normalize_ratio_value( 0.0 ) - 0.0 ) . abs( ) < 1e-6 ) ;
652+ assert ! ( ( openai_helpers:: normalize_ratio_value( 1.0 ) - 0.01 ) . abs( ) < 1e-6 ) ;
653+ assert ! ( ( openai_helpers:: normalize_ratio_value( 5.0 ) - 0.05 ) . abs( ) < 1e-6 ) ;
654+ assert ! ( ( openai_helpers:: normalize_ratio_value( 50.0 ) - 0.5 ) . abs( ) < 1e-6 ) ;
655+ assert ! ( ( openai_helpers:: normalize_ratio_value( 100.0 ) - 1.0 ) . abs( ) < 1e-6 ) ;
656+ // Out of range / weird inputs are clamped, not exploded.
657+ assert ! ( ( openai_helpers:: normalize_ratio_value( 150.0 ) - 1.0 ) . abs( ) < 1e-6 ) ;
658+ assert ! ( ( openai_helpers:: normalize_ratio_value( -5.0 ) - 0.0 ) . abs( ) < 1e-6 ) ;
659+ assert ! ( ( openai_helpers:: normalize_ratio_value( f32 :: NAN ) - 0.0 ) . abs( ) < 1e-6 ) ;
660+ }
661+
662+ #[ test]
663+ fn test_parse_openai_usage_payload_reports_low_percentages_correctly ( ) {
664+ // Regression for upstream PR #178 / issue #137.
665+ // The live `wham/usage` payload returns `used_percent` values in
666+ // `[0, 100]`. A weekly bucket reporting `1` (1% used) must not be
667+ // misclassified as a fully exhausted ratio of `1.0` (100% used). This
668+ // mirrors the shape of a real production response from
669+ // `https://chatgpt.com/backend-api/wham/usage`.
670+ let json = serde_json:: json!( {
671+ "plan_type" : "prolite" ,
672+ "rate_limit" : {
673+ "allowed" : true ,
674+ "limit_reached" : false ,
675+ "primary_window" : {
676+ "used_percent" : 5 ,
677+ "reset_at" : 1_778_283_299_i64
678+ } ,
679+ "secondary_window" : {
680+ "used_percent" : 1 ,
681+ "reset_at" : 1_778_870_099_i64
682+ }
683+ } ,
684+ "additional_rate_limits" : [ {
685+ "limit_name" : "GPT-5.3-Codex-Spark" ,
686+ "rate_limit" : {
687+ "allowed" : true ,
688+ "primary_window" : { "used_percent" : 5 , "reset_at" : 1_778_283_310_i64 } ,
689+ "secondary_window" : { "used_percent" : 1 , "reset_at" : 1_778_870_110_i64 }
690+ }
691+ } ]
692+ } ) ;
693+
694+ let parsed = openai_helpers:: parse_openai_usage_payload ( & json) ;
695+
696+ assert ! ( !parsed. hard_limit_reached) ;
697+ let by_name: std:: collections:: HashMap < _ , _ > = parsed
698+ . limits
699+ . iter ( )
700+ . map ( |l| ( l. name . as_str ( ) , l. usage_percent ) )
701+ . collect ( ) ;
702+ assert_eq ! ( by_name. get( "5-hour window" ) , Some ( & 5.0 ) ) ;
703+ assert_eq ! ( by_name. get( "7-day window" ) , Some ( & 1.0 ) ) ;
704+ assert_eq ! ( by_name. get( "GPT-5.3-Codex-Spark (5h)" ) , Some ( & 5.0 ) ) ;
705+ assert_eq ! ( by_name. get( "GPT-5.3-Codex-Spark (7d)" ) , Some ( & 1.0 ) ) ;
706+ }
0 commit comments