diff --git a/rust/src/providers/antigravity/tests.rs b/rust/src/providers/antigravity/tests.rs index 6d4c188145..557614da43 100644 --- a/rust/src/providers/antigravity/tests.rs +++ b/rust/src/providers/antigravity/tests.rs @@ -38,8 +38,8 @@ fn parses_current_language_server_process() { fn parses_language_server_without_extension_server_port() { let output = "34564\tC:\\Users\\test\\AppData\\Local\\Programs\\Antigravity\\resources\\bin\\language_server.exe --standalone --override_ide_name antigravity --subclient_type hub --override_ide_version 2.0.11 --https_server_port 0 --csrf_token 68dda2fb-6b26-40c0-aeef-b9a628615714 --app_data_dir antigravity"; - let process = AntigravityProvider::parse_process_info(output) - .expect("process info should be detected"); + let process = + AntigravityProvider::parse_process_info(output).expect("process info should be detected"); assert_eq!(process.pid, Some(34564)); assert_eq!(process.extension_port, Some(0)); @@ -51,8 +51,8 @@ fn parses_language_server_without_extension_server_port() { fn parses_language_server_without_any_port_arg() { let output = "34564\tC:\\Users\\test\\AppData\\Local\\Programs\\Antigravity\\resources\\bin\\language_server.exe --standalone --csrf_token aabbccdd-1122-3344-5566-778899001122 --app_data_dir antigravity"; - let process = AntigravityProvider::parse_process_info(output) - .expect("process info should be detected"); + let process = + AntigravityProvider::parse_process_info(output).expect("process info should be detected"); assert_eq!(process.pid, Some(34564)); assert_eq!(process.extension_port, None); @@ -64,8 +64,8 @@ fn parses_language_server_without_any_port_arg() { fn parses_equals_form_args() { let output = "34564\tC:\\Users\\test\\AppData\\Local\\Programs\\Antigravity\\resources\\bin\\language_server.exe --csrf_token=68dda2fb-6b26-40c0-aeef-b9a628615714 --https_server_port=61999"; - let process = AntigravityProvider::parse_process_info(output) - .expect("process info should be detected"); + let process = + AntigravityProvider::parse_process_info(output).expect("process info should be detected"); assert_eq!(process.pid, Some(34564)); assert_eq!(process.extension_port, Some(61999)); @@ -173,7 +173,8 @@ fn not_running_error_tells_user_how_to_start() { #[test] fn detects_agy_exe_cli_process_with_empty_csrf() { // agy.exe hosts the language server in-process with no --csrf_token. - let output = "7777\tC:\\Users\\test\\AppData\\Local\\agy\\bin\\agy.exe session --model gemini-2.5-pro"; + let output = + "7777\tC:\\Users\\test\\AppData\\Local\\agy\\bin\\agy.exe session --model gemini-2.5-pro"; let process = AntigravityProvider::parse_process_info(output) .expect("agy CLI process should be detected"); diff --git a/rust/src/providers/qwencloud/mod.rs b/rust/src/providers/qwencloud/mod.rs index b30beccbdc..957d8fb670 100644 --- a/rust/src/providers/qwencloud/mod.rs +++ b/rust/src/providers/qwencloud/mod.rs @@ -343,18 +343,30 @@ impl QwenCloudProvider { ), ) }); - let primary = five_hour - .or(legacy) - .ok_or_else(|| ProviderError::Parse("Qwen Cloud usage windows missing".into()))?; - let mut usage = UsageSnapshot::new(primary); - - if let Some(weekly_percent) = snapshot.weekly_used_percent { - usage = usage.with_secondary(RateWindow::with_details( - weekly_percent, + let weekly = snapshot.weekly_used_percent.map(|percent| { + RateWindow::with_details( + percent, Some(WEEKLY_MINUTES), snapshot.weekly_resets_at, - quota_detail_percent(weekly_percent, snapshot.weekly_total_quota), - )); + quota_detail_percent(percent, snapshot.weekly_total_quota), + ) + }); + + // Prefer the 5-hour window, then the legacy 30-day envelope. Individual + // Qwen Cloud plans expose only the weekly window (`per1WeekPercentage`); + // promote it to primary in that case instead of failing the whole fetch. + let (primary, secondary) = match (five_hour.or(legacy), weekly) { + (Some(primary), secondary) => (primary, secondary), + (None, Some(weekly)) => (weekly, None), + (None, None) => { + return Err(ProviderError::Parse( + "Qwen Cloud usage windows missing".into(), + )); + } + }; + let mut usage = UsageSnapshot::new(primary); + if let Some(secondary) = secondary { + usage = usage.with_secondary(secondary); } if let Some(plan) = snapshot.plan_name.filter(|plan| !plan.trim().is_empty()) { @@ -1388,4 +1400,36 @@ mod tests { assert!(!provider.metadata().default_enabled); assert_eq!(provider.metadata().dashboard_url, Some(DASHBOARD_URL)); } + + #[test] + fn weekly_only_shape_promotes_weekly_to_primary() { + // Real-world fixture: the account exposes only the weekly window, so + // there is no per5HourPercentage at all. Previously this failed with + // "Qwen Cloud usage windows missing"; now the weekly window becomes + // the primary window instead. + let payload = serde_json::json!({ + "code": "200", + "data": { + "DataV2": { + "data": { + "success": true, + "data": { + "per1WeekPercentage": 0.8439116574633999, + "per1WeekResetTime": 1785234900000_i64 + } + }, + "success": true, + "httpStatus": 200 + } + }, + "successResponse": true + }); + let usage = QwenCloudProvider::snapshot_to_usage( + QwenCloudProvider::parse(payload.to_string().as_bytes(), None, None).unwrap(), + ) + .unwrap(); + assert!((usage.primary.used_percent - 84.39116574634).abs() < 1e-9); + assert_eq!(usage.primary.window_minutes, Some(WEEKLY_MINUTES)); + assert!(usage.secondary.is_none()); + } }